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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squiffy-runtime",
3
- "version": "6.0.0-alpha.10",
3
+ "version": "6.0.0-alpha.12",
4
4
  "type": "module",
5
5
  "main": "dist/squiffy.runtime.js",
6
6
  "types": "dist/squiffy.runtime.d.ts",
@@ -22,7 +22,7 @@
22
22
  "@types/jsdom": "^21.1.7",
23
23
  "global-jsdom": "^25.0.0",
24
24
  "jsdom": "^25.0.0",
25
- "squiffy-compiler": "^6.0.0-alpha.2",
25
+ "squiffy-compiler": "^6.0.0-alpha.3",
26
26
  "typescript": "^5.6.2",
27
27
  "vitest": "^2.1.1"
28
28
  }
@@ -102,13 +102,3 @@ exports[`Update passage output - passage name "a'1" 2`] = `
102
102
  "
103
103
  <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a'1" role="link" tabindex="-1">a'1</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a'1]"><p>Updated passage content</p></div></div></div>"
104
104
  `;
105
-
106
- exports[`Update passage output 1`] = `
107
- "
108
- <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a]"><p>Passage a content</p></div></div></div>"
109
- `;
110
-
111
- exports[`Update passage output 2`] = `
112
- "
113
- <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div><div class="squiffy-output-block"><div data-source="[[_default]][a]"><p>Updated passage content</p></div></div></div>"
114
- `;
package/src/events.ts ADDED
@@ -0,0 +1,42 @@
1
+ type LinkType = 'section' | 'passage' | 'rotate' | 'sequence';
2
+
3
+ export type SquiffyEventMap = {
4
+ linkClick: { linkType: LinkType }; // a story link was clicked
5
+ };
6
+
7
+ export type SquiffyEventHandler<E extends keyof SquiffyEventMap> =
8
+ (payload: SquiffyEventMap[E]) => void;
9
+
10
+ export class Emitter<Events extends Record<string, any>> {
11
+ private listeners = new Map<keyof Events, Set<Function>>();
12
+
13
+ on<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
14
+ if (!this.listeners.has(event)) this.listeners.set(event, new Set());
15
+ this.listeners.get(event)!.add(handler);
16
+ return () => this.off(event, handler);
17
+ }
18
+
19
+ off<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
20
+ this.listeners.get(event)?.delete(handler);
21
+ }
22
+
23
+ once<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
24
+ const off = this.on(event, (payload) => {
25
+ off();
26
+ handler(payload);
27
+ });
28
+ return off;
29
+ }
30
+
31
+ emit<E extends keyof Events>(event: E, payload: Events[E]) {
32
+ // Fire handlers asynchronously so the runtime isn't blocked by user code.
33
+ queueMicrotask(() => {
34
+ this.listeners.get(event)?.forEach(h => {
35
+ try { (h as any)(payload); } catch (err) {
36
+ // Swallow so a bad handler doesn't break the game; optionally log.
37
+ console.error(`[Squiffy] handler for "${String(event)}" failed`, err);
38
+ }
39
+ });
40
+ });
41
+ }
42
+ }
@@ -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';
@@ -20,7 +20,6 @@ const html = `
20
20
 
21
21
  const compile = async (script: string) => {
22
22
  const compileResult = await squiffyCompile({
23
- scriptBaseFilename: "filename.squiffy", // TODO: This shouldn't be required
24
23
  script: script,
25
24
  });
26
25
 
@@ -40,7 +39,6 @@ const compile = async (script: string) => {
40
39
  }
41
40
 
42
41
  const initScript = async (script: string) => {
43
- globalJsdom(html);
44
42
  const element = document.getElementById('squiffy');
45
43
 
46
44
  if (!element) {
@@ -78,7 +76,7 @@ const getTestOutput = () => {
78
76
  let cleanup: { (): void };
79
77
 
80
78
  beforeEach(() => {
81
- cleanup = globalJsdom();
79
+ cleanup = globalJsdom(html);
82
80
  });
83
81
 
84
82
  afterEach(() => {
@@ -103,6 +101,9 @@ test('Click a section link', async () => {
103
101
  expect(section3Link).not.toBeNull();
104
102
  expect(linkToPassage.classList).not.toContain('disabled');
105
103
 
104
+ const handler = vi.fn();
105
+ const off = squiffyApi.on('linkClick', handler);
106
+
106
107
  const handled = squiffyApi.clickLink(section3Link);
107
108
  expect(handled).toBe(true);
108
109
 
@@ -110,6 +111,15 @@ test('Click a section link', async () => {
110
111
 
111
112
  // passage link is from the previous section, so should be unclickable
112
113
  expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
114
+
115
+ // await for the event to be processed
116
+ await Promise.resolve();
117
+
118
+ expect(handler).toHaveBeenCalledTimes(1);
119
+ expect(handler).toHaveBeenCalledWith(
120
+ expect.objectContaining({ linkType: 'section' })
121
+ );
122
+ off();
113
123
  });
114
124
 
115
125
  test('Click a passage link', async () => {
@@ -125,6 +135,9 @@ test('Click a passage link', async () => {
125
135
  expect(section3Link).not.toBeNull();
126
136
  expect(linkToPassage.classList).not.toContain('disabled');
127
137
 
138
+ const handler = vi.fn();
139
+ const off = squiffyApi.on('linkClick', handler);
140
+
128
141
  const handled = squiffyApi.clickLink(linkToPassage);
129
142
  expect(handled).toBe(true);
130
143
 
@@ -133,6 +146,15 @@ test('Click a passage link', async () => {
133
146
 
134
147
  // shouldn't be able to click it again
135
148
  expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
149
+
150
+ // await for the event to be processed
151
+ await Promise.resolve();
152
+
153
+ expect(handler).toHaveBeenCalledTimes(1);
154
+ expect(handler).toHaveBeenCalledWith(
155
+ expect.objectContaining({ linkType: 'passage' })
156
+ );
157
+ off();
136
158
  });
137
159
 
138
160
  test('Run JavaScript functions', async () => {
@@ -1,64 +1,9 @@
1
- export interface SquiffyInitOptions {
2
- element: HTMLElement;
3
- story: Story;
4
- scroll?: string,
5
- persist?: boolean,
6
- onSet?: (attribute: string, value: any) => void,
7
- }
1
+ import { SquiffyApi, SquiffyInitOptions, SquiffySettings, Story, Section } from './types.js';
2
+ import { startsWith, rotate } from "./utils.js";
3
+ import { TextProcessor } from './textProcessor.js';
4
+ import { Emitter, SquiffyEventMap } from './events.js';
8
5
 
9
- export interface SquiffySettings {
10
- scroll: string,
11
- persist: boolean,
12
- onSet: (attribute: string, value: any) => void,
13
- }
14
-
15
- export interface SquiffyApi {
16
- restart: () => void;
17
- get: (attribute: string) => any;
18
- set: (attribute: string, value: any) => void;
19
- clickLink: (link: HTMLElement) => boolean;
20
- update: (story: Story) => void;
21
- }
22
-
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)[];
42
- start: string;
43
- id?: string | null;
44
- sections: Record<string, Section>;
45
- }
46
-
47
- export interface Section {
48
- text?: string;
49
- clear?: boolean;
50
- attributes?: string[],
51
- jsIndex?: number;
52
- passages?: Record<string, Passage>;
53
- passageCount?: number;
54
- }
55
-
56
- export interface Passage {
57
- text?: string;
58
- clear?: boolean;
59
- attributes?: string[];
60
- jsIndex?: number;
61
- }
6
+ export type { SquiffyApi } from "./types.js"
62
7
 
63
8
  export const init = (options: SquiffyInitOptions): SquiffyApi => {
64
9
  let story: Story;
@@ -69,6 +14,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
69
14
  let outputElement: HTMLElement;
70
15
  let settings: SquiffySettings;
71
16
  let storageFallback: Record<string, string> = {};
17
+ let textProcessor: TextProcessor;
18
+ const emitter = new Emitter<SquiffyEventMap>();
72
19
 
73
20
  function set(attribute: string, value: any) {
74
21
  if (typeof value === 'undefined') value = true;
@@ -122,6 +69,7 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
122
69
  }
123
70
  }
124
71
 
72
+ emitter.emit('linkClick', { linkType: 'passage' });
125
73
  return true;
126
74
  }
127
75
 
@@ -131,6 +79,7 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
131
79
  go(section);
132
80
  }
133
81
 
82
+ emitter.emit('linkClick', { linkType: 'section' });
134
83
  return true;
135
84
  }
136
85
 
@@ -147,7 +96,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
147
96
  set(attribute, result[0]);
148
97
  }
149
98
  save();
150
-
99
+
100
+ emitter.emit('linkClick', { linkType: rotateAttr ? 'rotate' : 'sequence' });
151
101
  return true;
152
102
  }
153
103
 
@@ -493,197 +443,16 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
493
443
  }
494
444
  },
495
445
  processText: (text: string) => {
496
- function process(text: string, data: any) {
497
- let containsUnprocessedSection = false;
498
- const open = text.indexOf('{');
499
- let close;
500
-
501
- if (open > -1) {
502
- let nestCount = 1;
503
- let searchStart = open + 1;
504
- let finished = false;
505
-
506
- while (!finished) {
507
- const nextOpen = text.indexOf('{', searchStart);
508
- const nextClose = text.indexOf('}', searchStart);
509
-
510
- if (nextClose > -1) {
511
- if (nextOpen > -1 && nextOpen < nextClose) {
512
- nestCount++;
513
- searchStart = nextOpen + 1;
514
- }
515
- else {
516
- nestCount--;
517
- searchStart = nextClose + 1;
518
- if (nestCount === 0) {
519
- close = nextClose;
520
- containsUnprocessedSection = true;
521
- finished = true;
522
- }
523
- }
524
- }
525
- else {
526
- finished = true;
527
- }
528
- }
529
- }
530
-
531
- if (containsUnprocessedSection) {
532
- const section = text.substring(open + 1, close);
533
- const value = processTextCommand(section, data);
534
- text = text.substring(0, open) + value + process(text.substring(close! + 1), data);
535
- }
536
-
537
- return (text);
538
- }
539
-
540
- function processTextCommand(text: string, data: any) {
541
- if (startsWith(text, 'if ')) {
542
- return processTextCommand_If(text, data);
543
- }
544
- else if (startsWith(text, 'else:')) {
545
- return processTextCommand_Else(text, data);
546
- }
547
- else if (startsWith(text, 'label:')) {
548
- return processTextCommand_Label(text, data);
549
- }
550
- else if (/^rotate[: ]/.test(text)) {
551
- return processTextCommand_Rotate('rotate', text);
552
- }
553
- else if (/^sequence[: ]/.test(text)) {
554
- return processTextCommand_Rotate('sequence', text);
555
- }
556
- else if (currentSection.passages && text in currentSection.passages) {
557
- return process(currentSection.passages[text].text || '', data);
558
- }
559
- else if (text in story.sections) {
560
- return process(story.sections[text].text || '', data);
561
- }
562
- else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
563
- processAttributes(text.substring(1).split(","));
564
- return "";
565
- }
566
- return get(text);
567
- }
568
-
569
- function processTextCommand_If(section: string, data: any) {
570
- const command = section.substring(3);
571
- const colon = command.indexOf(':');
572
- if (colon == -1) {
573
- return ('{if ' + command + '}');
574
- }
575
-
576
- const text = command.substring(colon + 1);
577
- let condition = command.substring(0, colon);
578
- condition = condition.replace("<", "&lt;");
579
- const operatorRegex = /([\w ]*)(=|&lt;=|&gt;=|&lt;&gt;|&lt;|&gt;)(.*)/;
580
- const match = operatorRegex.exec(condition);
581
-
582
- let result = false;
583
-
584
- if (match) {
585
- const lhs = get(match[1]);
586
- const op = match[2];
587
- let rhs = match[3];
588
-
589
- if (startsWith(rhs, '@')) rhs = get(rhs.substring(1));
590
-
591
- if (op == '=' && lhs == rhs) result = true;
592
- if (op == '&lt;&gt;' && lhs != rhs) result = true;
593
- if (op == '&gt;' && lhs > rhs) result = true;
594
- if (op == '&lt;' && lhs < rhs) result = true;
595
- if (op == '&gt;=' && lhs >= rhs) result = true;
596
- if (op == '&lt;=' && lhs <= rhs) result = true;
597
- }
598
- else {
599
- let checkValue = true;
600
- if (startsWith(condition, 'not ')) {
601
- condition = condition.substring(4);
602
- checkValue = false;
603
- }
604
-
605
- if (startsWith(condition, 'seen ')) {
606
- result = (seen(condition.substring(5)) == checkValue);
607
- }
608
- else {
609
- let value = get(condition);
610
- if (value === null) value = false;
611
- result = (value == checkValue);
612
- }
613
- }
614
-
615
- const textResult = result ? process(text, data) : '';
616
-
617
- data.lastIf = result;
618
- return textResult;
619
- }
620
-
621
- function processTextCommand_Else(section: string, data: any) {
622
- if (!('lastIf' in data) || data.lastIf) return '';
623
- const text = section.substring(5);
624
- return process(text, data);
625
- }
626
-
627
- function processTextCommand_Label(section: string, data: any) {
628
- const command = section.substring(6);
629
- const eq = command.indexOf('=');
630
- if (eq == -1) {
631
- return ('{label:' + command + '}');
632
- }
633
-
634
- const text = command.substring(eq + 1);
635
- const label = command.substring(0, eq);
636
-
637
- return '<span class="squiffy-label-' + label + '">' + process(text, data) + '</span>';
638
- }
639
-
640
- function processTextCommand_Rotate(type: string, section: string) {
641
- let options;
642
- let attribute = '';
643
- if (section.substring(type.length, type.length + 1) == ' ') {
644
- const colon = section.indexOf(':');
645
- if (colon == -1) {
646
- return '{' + section + '}';
647
- }
648
- options = section.substring(colon + 1);
649
- attribute = section.substring(type.length + 1, colon);
650
- }
651
- else {
652
- options = section.substring(type.length + 1);
653
- }
654
- // TODO: Check - previously there was no second parameter here
655
- const rotation = rotate(options.replace(/"/g, '&quot;').replace(/'/g, '&#39;'), null);
656
- if (attribute) {
657
- set(attribute, rotation[0]);
658
- }
659
- return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
660
- }
661
-
662
446
  const data = {
663
447
  fulltext: text
664
448
  };
665
- return process(text, data);
449
+ return textProcessor.process(text, data);
666
450
  },
667
451
  transition: function (f: any) {
668
452
  set('_transition', f.toString());
669
453
  f();
670
454
  },
671
455
  };
672
-
673
- function startsWith(string: string, prefix: string) {
674
- return string.substring(0, prefix.length) === prefix;
675
- }
676
-
677
- function rotate(options: string, current: string | null) {
678
- const colon = options.indexOf(':');
679
- if (colon == -1) {
680
- return [options, current];
681
- }
682
- const next = options.substring(0, colon);
683
- let remaining = options.substring(colon + 1);
684
- if (current) remaining += ':' + current;
685
- return [next, remaining];
686
- }
687
456
 
688
457
  function safeQuerySelector(name: string) {
689
458
  return name.replace(/'/g, "\\'");
@@ -784,6 +553,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
784
553
  if (event.key !== "Enter") return;
785
554
  handleClick(event);
786
555
  });
556
+
557
+ textProcessor = new TextProcessor(get, set, story, ui, seen, processAttributes);
787
558
 
788
559
  begin();
789
560
 
@@ -793,5 +564,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
793
564
  set: set,
794
565
  clickLink: handleLink,
795
566
  update: update,
567
+ on: (e, h) => emitter.on(e, h),
568
+ off: (e, h) => emitter.off(e, h),
569
+ once: (e, h) => emitter.once(e, h),
796
570
  };
797
571
  };
@@ -0,0 +1,177 @@
1
+ import { startsWith, rotate } from "./utils.js";
2
+
3
+ export class TextProcessor {
4
+ get: (attribute: string) => any;
5
+ set: (attribute: string, value: any) => void;
6
+ story: any;
7
+ currentSection: any;
8
+ seen: (section: string) => boolean;
9
+ processAttributes: (attributes: string[]) => void;
10
+
11
+ constructor (get: (attribute: string) => any,
12
+ set: (attribute: string, value: any) => void,
13
+ story: any, currentSection: any,
14
+ seen: (section: string) => boolean,
15
+ processAttributes: (attributes: string[]) => void) {
16
+ this.get = get;
17
+ this.set = set;
18
+ this.story = story;
19
+ this.currentSection = currentSection;
20
+ this.seen = seen;
21
+ this.processAttributes = processAttributes;
22
+ }
23
+
24
+ process(text: string, data: any) {
25
+ let containsUnprocessedSection = false;
26
+ const open = text.indexOf('{');
27
+ let close;
28
+
29
+ if (open > -1) {
30
+ let nestCount = 1;
31
+ let searchStart = open + 1;
32
+ let finished = false;
33
+
34
+ while (!finished) {
35
+ const nextOpen = text.indexOf('{', searchStart);
36
+ const nextClose = text.indexOf('}', searchStart);
37
+
38
+ if (nextClose > -1) {
39
+ if (nextOpen > -1 && nextOpen < nextClose) {
40
+ nestCount++;
41
+ searchStart = nextOpen + 1;
42
+ } else {
43
+ nestCount--;
44
+ searchStart = nextClose + 1;
45
+ if (nestCount === 0) {
46
+ close = nextClose;
47
+ containsUnprocessedSection = true;
48
+ finished = true;
49
+ }
50
+ }
51
+ } else {
52
+ finished = true;
53
+ }
54
+ }
55
+ }
56
+
57
+ if (containsUnprocessedSection) {
58
+ const section = text.substring(open + 1, close);
59
+ const value = this.processTextCommand(section, data);
60
+ text = text.substring(0, open) + value + this.process(text.substring(close! + 1), data);
61
+ }
62
+
63
+ return (text);
64
+ }
65
+
66
+ processTextCommand(text: string, data: any) {
67
+ if (startsWith(text, 'if ')) {
68
+ return this.processTextCommand_If(text, data);
69
+ } else if (startsWith(text, 'else:')) {
70
+ return this.processTextCommand_Else(text, data);
71
+ } else if (startsWith(text, 'label:')) {
72
+ return this.processTextCommand_Label(text, data);
73
+ } else if (/^rotate[: ]/.test(text)) {
74
+ return this.processTextCommand_Rotate('rotate', text);
75
+ } else if (/^sequence[: ]/.test(text)) {
76
+ return this.processTextCommand_Rotate('sequence', text);
77
+ } else if (this.currentSection.passages && text in this.currentSection.passages) {
78
+ return this.process(this.currentSection.passages[text].text || '', data);
79
+ } else if (text in this.story.sections) {
80
+ return this.process(this.story.sections[text].text || '', data);
81
+ } else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
82
+ this.processAttributes(text.substring(1).split(","));
83
+ return "";
84
+ }
85
+ return this.get(text);
86
+ }
87
+
88
+ processTextCommand_If(section: string, data: any) {
89
+ const command = section.substring(3);
90
+ const colon = command.indexOf(':');
91
+ if (colon == -1) {
92
+ return ('{if ' + command + '}');
93
+ }
94
+
95
+ const text = command.substring(colon + 1);
96
+ let condition = command.substring(0, colon);
97
+ condition = condition.replace("<", "&lt;");
98
+ const operatorRegex = /([\w ]*)(=|&lt;=|&gt;=|&lt;&gt;|&lt;|&gt;)(.*)/;
99
+ const match = operatorRegex.exec(condition);
100
+
101
+ let result = false;
102
+
103
+ if (match) {
104
+ const lhs = this.get(match[1]);
105
+ const op = match[2];
106
+ let rhs = match[3];
107
+
108
+ if (startsWith(rhs, '@')) rhs = this.get(rhs.substring(1));
109
+
110
+ if (op == '=' && lhs == rhs) result = true;
111
+ if (op == '&lt;&gt;' && lhs != rhs) result = true;
112
+ if (op == '&gt;' && lhs > rhs) result = true;
113
+ if (op == '&lt;' && lhs < rhs) result = true;
114
+ if (op == '&gt;=' && lhs >= rhs) result = true;
115
+ if (op == '&lt;=' && lhs <= rhs) result = true;
116
+ } else {
117
+ let checkValue = true;
118
+ if (startsWith(condition, 'not ')) {
119
+ condition = condition.substring(4);
120
+ checkValue = false;
121
+ }
122
+
123
+ if (startsWith(condition, 'seen ')) {
124
+ result = (this.seen(condition.substring(5)) == checkValue);
125
+ } else {
126
+ let value = this.get(condition);
127
+ if (value === null) value = false;
128
+ result = (value == checkValue);
129
+ }
130
+ }
131
+
132
+ const textResult = result ? this.process(text, data) : '';
133
+
134
+ data.lastIf = result;
135
+ return textResult;
136
+ }
137
+
138
+ processTextCommand_Else(section: string, data: any) {
139
+ if (!('lastIf' in data) || data.lastIf) return '';
140
+ const text = section.substring(5);
141
+ return this.process(text, data);
142
+ }
143
+
144
+ processTextCommand_Label(section: string, data: any) {
145
+ const command = section.substring(6);
146
+ const eq = command.indexOf('=');
147
+ if (eq == -1) {
148
+ return ('{label:' + command + '}');
149
+ }
150
+
151
+ const text = command.substring(eq + 1);
152
+ const label = command.substring(0, eq);
153
+
154
+ return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
155
+ }
156
+
157
+ processTextCommand_Rotate(type: string, section: string) {
158
+ let options;
159
+ let attribute = '';
160
+ if (section.substring(type.length, type.length + 1) == ' ') {
161
+ const colon = section.indexOf(':');
162
+ if (colon == -1) {
163
+ return '{' + section + '}';
164
+ }
165
+ options = section.substring(colon + 1);
166
+ attribute = section.substring(type.length + 1, colon);
167
+ } else {
168
+ options = section.substring(type.length + 1);
169
+ }
170
+ // TODO: Check - previously there was no second parameter here
171
+ const rotation = rotate(options.replace(/"/g, '&quot;').replace(/'/g, '&#39;'), null);
172
+ if (attribute) {
173
+ this.set(attribute, rotation[0]);
174
+ }
175
+ return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
176
+ }
177
+ }