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

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.11",
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,7 @@
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
-
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
- }
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';
62
5
 
63
6
  export const init = (options: SquiffyInitOptions): SquiffyApi => {
64
7
  let story: Story;
@@ -69,6 +12,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
69
12
  let outputElement: HTMLElement;
70
13
  let settings: SquiffySettings;
71
14
  let storageFallback: Record<string, string> = {};
15
+ let textProcessor: TextProcessor;
16
+ const emitter = new Emitter<SquiffyEventMap>();
72
17
 
73
18
  function set(attribute: string, value: any) {
74
19
  if (typeof value === 'undefined') value = true;
@@ -122,6 +67,7 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
122
67
  }
123
68
  }
124
69
 
70
+ emitter.emit('linkClick', { linkType: 'passage' });
125
71
  return true;
126
72
  }
127
73
 
@@ -131,6 +77,7 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
131
77
  go(section);
132
78
  }
133
79
 
80
+ emitter.emit('linkClick', { linkType: 'section' });
134
81
  return true;
135
82
  }
136
83
 
@@ -147,7 +94,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
147
94
  set(attribute, result[0]);
148
95
  }
149
96
  save();
150
-
97
+
98
+ emitter.emit('linkClick', { linkType: rotateAttr ? 'rotate' : 'sequence' });
151
99
  return true;
152
100
  }
153
101
 
@@ -493,197 +441,16 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
493
441
  }
494
442
  },
495
443
  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
444
  const data = {
663
445
  fulltext: text
664
446
  };
665
- return process(text, data);
447
+ return textProcessor.process(text, data);
666
448
  },
667
449
  transition: function (f: any) {
668
450
  set('_transition', f.toString());
669
451
  f();
670
452
  },
671
453
  };
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
454
 
688
455
  function safeQuerySelector(name: string) {
689
456
  return name.replace(/'/g, "\\'");
@@ -784,6 +551,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
784
551
  if (event.key !== "Enter") return;
785
552
  handleClick(event);
786
553
  });
554
+
555
+ textProcessor = new TextProcessor(get, set, story, ui, seen, processAttributes);
787
556
 
788
557
  begin();
789
558
 
@@ -793,5 +562,8 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
793
562
  set: set,
794
563
  clickLink: handleLink,
795
564
  update: update,
565
+ on: (e, h) => emitter.on(e, h),
566
+ off: (e, h) => emitter.off(e, h),
567
+ once: (e, h) => emitter.once(e, h),
796
568
  };
797
569
  };
@@ -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
+ }