clarity-pattern-parser 11.4.1 → 11.4.2

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.
@@ -42,7 +42,7 @@ export declare class AutoComplete {
42
42
  * ie. sequence pattern segments ≈ [{look}, {an example}, {phrase}]
43
43
  * fullText = "look an"
44
44
  * remove {look} segment as its already been completed by the existing text.
45
- */
45
+ */
46
46
  private _filterCompletedSubSegments;
47
47
  private _getCompositeSuggestionsForPattern;
48
48
  private _getCustomTokens;
@@ -3,7 +3,9 @@ import { Match } from "./CursorHistory";
3
3
  import { ParseError } from "./ParseError";
4
4
  import { Pattern } from "./Pattern";
5
5
  export declare class Cursor {
6
- private _chars;
6
+ private _text;
7
+ private _charSize;
8
+ private _charMap;
7
9
  private _index;
8
10
  private _length;
9
11
  private _history;
@@ -33,10 +35,13 @@ export declare class Cursor {
33
35
  moveToFirstChar(): void;
34
36
  moveToLastChar(): void;
35
37
  getLastIndex(): number;
36
- getChars(first: number, last: number): string;
38
+ substring(first: number, last: number): string;
37
39
  recordMatch(pattern: Pattern, node: Node): void;
38
40
  recordErrorAt(startIndex: number, lastIndex: number, onPattern: Pattern): void;
39
41
  resolveError(): void;
40
42
  startRecording(): void;
41
43
  stopRecording(): void;
44
+ getCharStartIndex(index: number): number;
45
+ getCharEndIndex(index: number): number;
46
+ getCharLastIndex(index: number): number;
42
47
  }
@@ -8,10 +8,8 @@ export declare class Literal implements Pattern {
8
8
  private _name;
9
9
  private _parent;
10
10
  private _token;
11
- private _runes;
12
11
  private _firstIndex;
13
12
  private _lastIndex;
14
- private _endIndex;
15
13
  get id(): string;
16
14
  get type(): string;
17
15
  get name(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-pattern-parser",
3
- "version": "11.4.1",
3
+ "version": "11.4.2",
4
4
  "description": "Parsing Library for Typescript and Javascript.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.esm.js",
@@ -194,7 +194,7 @@ describe("AutoComplete", () => {
194
194
 
195
195
  expect(result.ast).toBeNull();
196
196
  optionsMatchExpected(result.options, expectedOptions);
197
- expect(result.errorAtIndex).toBe(text.length);
197
+ expect(result.errorAtIndex).toBe(text.length - 1);
198
198
  expect(result.isComplete).toBeFalsy();
199
199
  expect(result.cursor).not.toBeNull();
200
200
  });
@@ -313,7 +313,7 @@ describe("AutoComplete", () => {
313
313
 
314
314
  expect(result.ast).toBeNull();
315
315
  optionsMatchExpected(result.options, expectedOptions);
316
- expect(result.errorAtIndex).toBe(2);
316
+ expect(result.errorAtIndex).toBe(1);
317
317
  expect(result.isComplete).toBeFalsy();
318
318
  expect(result.cursor).not.toBeNull();
319
319
  });
@@ -944,7 +944,7 @@ describe("AutoComplete", () => {
944
944
 
945
945
  optionsMatchExpected(suggestion.options, expected);
946
946
 
947
- expect(suggestion.error?.lastIndex).toBe(2);
947
+ expect(suggestion.error?.lastIndex).toBe(1);
948
948
  });
949
949
 
950
950
  test("Recursion With And", () => {
@@ -107,7 +107,7 @@ export class AutoComplete {
107
107
  const furthestMatch = cursor.allMatchedNodes[cursor.allMatchedNodes.length - 1];
108
108
 
109
109
  if (furthestError && furthestMatch) {
110
- if (furthestMatch.endIndex > furthestError.lastIndex ) {
110
+ if (furthestMatch.endIndex > furthestError.lastIndex) {
111
111
  return furthestMatch.endIndex;
112
112
  } else {
113
113
  return furthestError.lastIndex;
@@ -131,7 +131,7 @@ export class AutoComplete {
131
131
 
132
132
  const errorMatchOptions = this._createSuggestionOptionsFromErrors();
133
133
  const leafMatchOptions = this._cursor.leafMatches.map((m) => this._createSuggestionOptionsFromMatch(m)).flat();
134
-
134
+
135
135
  const finalResults: SuggestionOption[] = [];
136
136
  [...leafMatchOptions, ...errorMatchOptions].forEach(m => {
137
137
  const index = finalResults.findIndex(f => m.text === f.text);
@@ -145,15 +145,15 @@ export class AutoComplete {
145
145
 
146
146
  private _createSuggestionOptionsFromErrors() {
147
147
  // These errored because the length of the string.
148
- const errors = this._cursor.errors.filter(e => e.lastIndex === this._cursor.length);
149
-
148
+ const errors = this._cursor.errors.filter(e => e.lastIndex === this._cursor.length - 1);
149
+
150
150
  const errorSuggestionOptions = errors.map(parseError => {
151
151
 
152
- const currentText = this._cursor.getChars(parseError.startIndex, parseError.lastIndex);
153
-
152
+ const currentText = this._cursor.substring(parseError.startIndex, parseError.lastIndex);
153
+
154
154
  const compositeSuggestions = this._getCompositeSuggestionsForPattern(parseError.pattern);
155
155
  const trimmedErrorCompositeSuggestions = this._trimSuggestionsByExistingText(currentText, compositeSuggestions);
156
-
156
+
157
157
  return this._createSuggestions(parseError.lastIndex, trimmedErrorCompositeSuggestions);
158
158
  }).flat();
159
159
 
@@ -170,25 +170,25 @@ export class AutoComplete {
170
170
  return this._createSuggestions(-1, compositeSuggestions);
171
171
  }
172
172
 
173
- if ( match?.node != null) {
174
- const currentText = this._text.slice(match.node.startIndex,match.node.endIndex)
175
-
176
-
173
+ if (match?.node != null) {
174
+ const currentText = this._text.slice(match.node.startIndex, match.node.endIndex)
175
+
176
+
177
177
  /**Captures suggestions for a "completed" match pattern that still has existing possible suggestions.
178
178
  * particularly relevant when working with set/custom tokens.
179
179
  */
180
180
  const matchCompositeSuggestions = this._getCompositeSuggestionsForPattern(match.pattern)
181
- const trimmedMatchCompositeSuggestions = this._trimSuggestionsByExistingText(currentText, matchCompositeSuggestions)
181
+ const trimmedMatchCompositeSuggestions = this._trimSuggestionsByExistingText(currentText, matchCompositeSuggestions)
182
+
182
183
 
183
-
184
184
  const leafPatterns = match.pattern.getNextPatterns();
185
- const leafCompositeSuggestions = leafPatterns.flatMap(leafPattern =>
186
- this._getCompositeSuggestionsForPattern(leafPattern)
187
- );
185
+ const leafCompositeSuggestions = leafPatterns.flatMap(leafPattern =>
186
+ this._getCompositeSuggestionsForPattern(leafPattern)
187
+ );
188
188
 
189
- const allCompositeSuggestions = [...leafCompositeSuggestions,...trimmedMatchCompositeSuggestions,]
189
+ const allCompositeSuggestions = [...leafCompositeSuggestions, ...trimmedMatchCompositeSuggestions,]
190
190
 
191
- const dedupedCompositeSuggestions = this._deDupeCompositeSuggestions(allCompositeSuggestions);
191
+ const dedupedCompositeSuggestions = this._deDupeCompositeSuggestions(allCompositeSuggestions);
192
192
 
193
193
  return this._createSuggestions(match.node.lastIndex, dedupedCompositeSuggestions);
194
194
  } else {
@@ -202,58 +202,58 @@ export class AutoComplete {
202
202
  * - refines to {d}{ef}
203
203
  */
204
204
  private _trimSuggestionsByExistingText(currentText: string, compositeSuggestions: CompositeSuggestion[]): CompositeSuggestion[] {
205
-
206
- const trimmedSuggestions = compositeSuggestions.reduce<CompositeSuggestion[]>((acc, compositeSuggestion) => {
207
- if (compositeSuggestion.text.startsWith(currentText)) {
208
-
209
- const filteredSegments = this._filterCompletedSubSegments(currentText, compositeSuggestion);
210
- const slicedSuggestionText = compositeSuggestion.text.slice(currentText.length);
211
-
212
- if (slicedSuggestionText !== '') {
213
- const refinedCompositeSuggestion: CompositeSuggestion = {
214
- text: slicedSuggestionText,
215
- suggestionSequence: filteredSegments,
216
- }
217
-
218
- acc.push(refinedCompositeSuggestion);
219
- }
205
+
206
+ const trimmedSuggestions = compositeSuggestions.reduce<CompositeSuggestion[]>((acc, compositeSuggestion) => {
207
+ if (compositeSuggestion.text.startsWith(currentText)) {
208
+
209
+ const filteredSegments = this._filterCompletedSubSegments(currentText, compositeSuggestion);
210
+ const slicedSuggestionText = compositeSuggestion.text.slice(currentText.length);
211
+
212
+ if (slicedSuggestionText !== '') {
213
+ const refinedCompositeSuggestion: CompositeSuggestion = {
214
+ text: slicedSuggestionText,
215
+ suggestionSequence: filteredSegments,
220
216
  }
221
- return acc;
222
- }, []);
223
217
 
224
- return trimmedSuggestions
218
+ acc.push(refinedCompositeSuggestion);
219
+ }
220
+ }
221
+ return acc;
222
+ }, []);
223
+
224
+ return trimmedSuggestions
225
225
  }
226
226
 
227
- /** Removed segments already accounted for in the existing text.
228
- * ie. sequence pattern segments ≈ [{look}, {an example}, {phrase}]
229
- * fullText = "look an"
230
- * remove {look} segment as its already been completed by the existing text.
231
- */
232
- private _filterCompletedSubSegments(currentText: string, compositeSuggestion: CompositeSuggestion){
233
-
234
- let elementsToRemove: SuggestionSegment [] = [];
235
- let workingText = currentText;
236
-
237
- compositeSuggestion.suggestionSequence.forEach(segment => {
238
- /**sub segment has been completed, remove it from the sequence */
239
- if(workingText.startsWith(segment.text)){
240
- workingText = workingText.slice(segment.text.length);
241
- elementsToRemove.push (segment);
242
- }
243
- })
244
-
245
- const filteredSegments = compositeSuggestion.suggestionSequence.filter(segment => !elementsToRemove.includes (segment) );
246
-
247
- return filteredSegments
248
- }
227
+ /** Removed segments already accounted for in the existing text.
228
+ * ie. sequence pattern segments ≈ [{look}, {an example}, {phrase}]
229
+ * fullText = "look an"
230
+ * remove {look} segment as its already been completed by the existing text.
231
+ */
232
+ private _filterCompletedSubSegments(currentText: string, compositeSuggestion: CompositeSuggestion) {
233
+
234
+ let elementsToRemove: SuggestionSegment[] = [];
235
+ let workingText = currentText;
236
+
237
+ compositeSuggestion.suggestionSequence.forEach(segment => {
238
+ /**sub segment has been completed, remove it from the sequence */
239
+ if (workingText.startsWith(segment.text)) {
240
+ workingText = workingText.slice(segment.text.length);
241
+ elementsToRemove.push(segment);
242
+ }
243
+ })
244
+
245
+ const filteredSegments = compositeSuggestion.suggestionSequence.filter(segment => !elementsToRemove.includes(segment));
246
+
247
+ return filteredSegments
248
+ }
249
+
250
+ private _getCompositeSuggestionsForPattern(pattern: Pattern): CompositeSuggestion[] {
249
251
 
250
- private _getCompositeSuggestionsForPattern(pattern: Pattern):CompositeSuggestion[] {
252
+ const suggestionsToReturn: CompositeSuggestion[] = [];
251
253
 
252
- const suggestionsToReturn:CompositeSuggestion[] = [];
253
-
254
254
  const leafPatterns = pattern.getPatterns();
255
255
  // for when pattern has no leafPatterns and only returns itself
256
- if(leafPatterns.length === 1 && leafPatterns[0].id === pattern.id) {
256
+ if (leafPatterns.length === 1 && leafPatterns[0].id === pattern.id) {
257
257
 
258
258
  const currentCustomTokens = this._getCustomTokens(pattern);
259
259
  const currentTokens = pattern.getTokens();
@@ -261,12 +261,12 @@ export class AutoComplete {
261
261
 
262
262
  const leafCompositeSuggestions: CompositeSuggestion[] = allTokens.map(token => {
263
263
 
264
- const segment:SuggestionSegment = {
264
+ const segment: SuggestionSegment = {
265
265
  text: token,
266
266
  pattern: pattern,
267
267
  }
268
-
269
- const compositeSuggestion:CompositeSuggestion = {
268
+
269
+ const compositeSuggestion: CompositeSuggestion = {
270
270
  text: token,
271
271
  suggestionSequence: [segment],
272
272
  }
@@ -274,17 +274,17 @@ export class AutoComplete {
274
274
  })
275
275
  suggestionsToReturn.push(...leafCompositeSuggestions);
276
276
 
277
- }else{
277
+ } else {
278
278
 
279
279
  const currentCustomTokens = this._getCustomTokens(pattern);
280
280
 
281
281
  const patternsSuggestionList = currentCustomTokens.map(token => {
282
- const segment:SuggestionSegment = {
282
+ const segment: SuggestionSegment = {
283
283
  text: token,
284
284
  pattern: pattern,
285
285
  }
286
286
 
287
- const patternSuggestion:CompositeSuggestion = {
287
+ const patternSuggestion: CompositeSuggestion = {
288
288
  text: token,
289
289
  suggestionSequence: [segment],
290
290
  }
@@ -306,15 +306,15 @@ export class AutoComplete {
306
306
 
307
307
  return acc;
308
308
  }, []);
309
-
310
- const compositeSuggestionList:CompositeSuggestion[] = [];
311
-
309
+
310
+ const compositeSuggestionList: CompositeSuggestion[] = [];
311
+
312
312
  for (const currentSuggestion of suggestionsToReturn) {
313
- for (const nextSuggestionWithSubElements of nextPatternedTokensList) {
313
+ for (const nextSuggestionWithSubElements of nextPatternedTokensList) {
314
314
 
315
- const augmentedTokenWithPattern:CompositeSuggestion = {
315
+ const augmentedTokenWithPattern: CompositeSuggestion = {
316
316
  text: currentSuggestion.text + nextSuggestionWithSubElements.text,
317
- suggestionSequence: [...currentSuggestion.suggestionSequence,...nextSuggestionWithSubElements.suggestionSequence ],
317
+ suggestionSequence: [...currentSuggestion.suggestionSequence, ...nextSuggestionWithSubElements.suggestionSequence],
318
318
  }
319
319
 
320
320
  compositeSuggestionList.push(augmentedTokenWithPattern);
@@ -323,7 +323,7 @@ export class AutoComplete {
323
323
 
324
324
  return compositeSuggestionList;
325
325
 
326
- } else {
326
+ } else {
327
327
 
328
328
  const dedupedSuggestions = this._deDupeCompositeSuggestions(suggestionsToReturn);
329
329
  return dedupedSuggestions;
@@ -332,16 +332,16 @@ export class AutoComplete {
332
332
 
333
333
  private _getCustomTokens(pattern: Pattern) {
334
334
 
335
- const customTokensMap: Record<string, string[]> = this._options.customTokens || {};
336
- const customTokens = customTokensMap[pattern.name] ?? [];
335
+ const customTokensMap: Record<string, string[]> = this._options.customTokens || {};
336
+ const customTokens = customTokensMap[pattern.name] ?? [];
337
337
 
338
- const allTokens = [...customTokens];
338
+ const allTokens = [...customTokens];
339
339
 
340
- return allTokens;
340
+ return allTokens;
341
341
  }
342
342
 
343
343
 
344
-
344
+
345
345
  private _deDupeCompositeSuggestions<T extends CompositeSuggestion>(suggestions: T[]): T[] {
346
346
 
347
347
  if (this._options.disableDedupe) {
@@ -370,9 +370,9 @@ export class AutoComplete {
370
370
  return unique;
371
371
  }
372
372
 
373
- private _createSuggestions(lastIndex: number, compositeSuggestionList:CompositeSuggestion[]): SuggestionOption[] {
373
+ private _createSuggestions(lastIndex: number, compositeSuggestionList: CompositeSuggestion[]): SuggestionOption[] {
374
374
 
375
- let textToIndex = lastIndex === -1 ? "" : this._cursor.getChars(0, lastIndex);
375
+ let textToIndex = lastIndex === -1 ? "" : this._cursor.substring(0, lastIndex);
376
376
  const suggestionStrings: string[] = [];
377
377
  const options: SuggestionOption[] = [];
378
378
 
@@ -384,9 +384,9 @@ export class AutoComplete {
384
384
  const isSameAsText = existingTextWithSuggestion === this._text;
385
385
 
386
386
  // if ( !alreadyExist && !isSameAsText) {
387
- suggestionStrings.push(existingTextWithSuggestion);
388
- const suggestionOption = this._createSuggestionOption(this._cursor.text, existingTextWithSuggestion, compositeSuggestion.suggestionSequence);
389
- options.push(suggestionOption);
387
+ suggestionStrings.push(existingTextWithSuggestion);
388
+ const suggestionOption = this._createSuggestionOption(this._cursor.text, existingTextWithSuggestion, compositeSuggestion.suggestionSequence);
389
+ options.push(suggestionOption);
390
390
  // }
391
391
  }
392
392
 
@@ -400,7 +400,7 @@ export class AutoComplete {
400
400
  const furthestMatch = findMatchIndex(suggestion, fullText);
401
401
  const text = suggestion.slice(furthestMatch);
402
402
 
403
- const option:SuggestionOption = {
403
+ const option: SuggestionOption = {
404
404
  text: text,
405
405
  startIndex: furthestMatch,
406
406
  suggestionSequence: segments,
@@ -10,7 +10,7 @@ const doubleQuoteStringLiteral = new Sequence("double-string-literal", [
10
10
  new Literal("double-quote", "\""),
11
11
  new Optional("optional-characters", new Repeat("characters",
12
12
  new Options("characters", [
13
- new Regex("normal-characters", "[^\\\"]+"),
13
+ new Regex("normal-characters", '[^"]+'),
14
14
  escapedCharacter
15
15
  ])
16
16
  )),
@@ -21,7 +21,7 @@ const singleQuoteStringLiteral = new Sequence("single-string-literal", [
21
21
  new Literal("single-quote", "'"),
22
22
  new Optional("optional-characters", new Repeat("characters",
23
23
  new Options("characters", [
24
- new Regex("normal-characters", "[^\\']+"),
24
+ new Regex("normal-characters", "[^']+"),
25
25
  escapedCharacter
26
26
  ]),
27
27
  )),
@@ -27,7 +27,7 @@ describe("Cursor", () => {
27
27
  cursor.previous();
28
28
  expect(cursor.index).toBe(0);
29
29
 
30
- const text = cursor.getChars(0, 1);
30
+ const text = cursor.substring(0, 1);
31
31
  expect(text).toBe("");
32
32
  });
33
33
 
@@ -107,7 +107,7 @@ describe("Cursor", () => {
107
107
 
108
108
  test("Text Information", () => {
109
109
  const cursor = new Cursor("Hello World!");
110
- const hello = cursor.getChars(0, 4);
110
+ const hello = cursor.substring(0, 4);
111
111
 
112
112
  expect(hello).toBe("Hello");
113
113
  expect(cursor.length).toBe(12);
@@ -153,4 +153,12 @@ describe("Cursor", () => {
153
153
  expect(records[2].pattern.name).toBe("first-names");
154
154
  });
155
155
 
156
+ test("Text with Emojis", () => {
157
+ const cursor = new Cursor("🔴 World!");
158
+ expect(cursor.currentChar).toBe("🔴");
159
+ cursor.next();
160
+ expect(cursor.currentChar).toBe(" ");
161
+ expect(cursor.length).toBe(9);
162
+ });
163
+
156
164
  });
@@ -3,14 +3,18 @@ import { CursorHistory, Match } from "./CursorHistory";
3
3
  import { ParseError } from "./ParseError";
4
4
  import { Pattern } from "./Pattern";
5
5
 
6
+ const segmenter = new Intl.Segmenter("und", { granularity: "grapheme" });
7
+
6
8
  export class Cursor {
7
- private _chars: string[];
9
+ private _text: string;
10
+ private _charSize: number[];
11
+ private _charMap: number[];
8
12
  private _index: number;
9
13
  private _length: number;
10
14
  private _history: CursorHistory;
11
15
 
12
16
  get text(): string {
13
- return this._chars.join("");
17
+ return this._text;
14
18
  }
15
19
 
16
20
  get isOnFirst(): boolean {
@@ -74,39 +78,62 @@ export class Cursor {
74
78
  }
75
79
 
76
80
  get currentChar(): string {
77
- return this._chars[this._index];
81
+ const index = this.getCharStartIndex(this._index);
82
+ return this.text.slice(index, index + this._charSize[index]);
78
83
  }
79
84
 
80
85
  constructor(text: string) {
81
- this._chars = [...text];
86
+ this._text = text;
87
+ this._length = text.length;
88
+ this._charSize = [];
89
+ this._charMap = [];
90
+
82
91
  this._index = 0;
83
- this._length = this._chars.length;
84
92
  this._history = new CursorHistory();
93
+
94
+ let index = 0;
95
+ for (const segment of segmenter.segment(text)) {
96
+ const size = segment.segment.length;
97
+ for (let i = 0; i < size; i++) {
98
+ this._charMap.push(index);
99
+ this._charSize.push(size);
100
+ }
101
+ index += size;
102
+ }
85
103
  }
86
104
 
87
105
  hasNext(): boolean {
88
- return this._index + 1 < this._length;
106
+ const index = this._charMap[this._index];
107
+ const charSize = this._charSize[index];
108
+ return index + charSize < this._length;
89
109
  }
90
110
 
91
111
  next(): void {
92
112
  if (this.hasNext()) {
93
- this._index++;
113
+ const index = this._charMap[this._index];
114
+ const size = this._charSize[index];
115
+ this.moveTo(index + size);
94
116
  }
95
117
  }
96
118
 
97
119
  hasPrevious(): boolean {
98
- return this._index - 1 >= 0;
120
+ const index = this._charMap[this._index];
121
+ const previousIndex = this._charMap[index - 1] ?? -1;
122
+
123
+ return previousIndex >= 0;
99
124
  }
100
125
 
101
126
  previous(): void {
102
127
  if (this.hasPrevious()) {
103
- this._index--;
128
+ const index = this._charMap[this._index];
129
+ const previousIndex = this._charMap[index - 1] ?? -1;;
130
+ this.moveTo(previousIndex);
104
131
  }
105
132
  }
106
133
 
107
134
  moveTo(position: number): void {
108
135
  if (position >= 0 && position < this._length) {
109
- this._index = position;
136
+ this._index = this._charMap[position];
110
137
  }
111
138
  }
112
139
 
@@ -122,8 +149,8 @@ export class Cursor {
122
149
  return this._length - 1;
123
150
  }
124
151
 
125
- getChars(first: number, last: number): string {
126
- return this._chars.slice(first, last + 1).join("");
152
+ substring(first: number, last: number): string {
153
+ return this._text.slice(first, last + 1);
127
154
  }
128
155
 
129
156
  recordMatch(pattern: Pattern, node: Node): void {
@@ -146,4 +173,17 @@ export class Cursor {
146
173
  this._history.stopRecording();
147
174
  }
148
175
 
176
+ getCharStartIndex(index: number): number {
177
+ return this._charMap[index];
178
+ }
179
+
180
+ getCharEndIndex(index: number): number {
181
+ let startIndex = this.getCharStartIndex(index);
182
+ return startIndex + this._charSize[startIndex] ?? 1;
183
+ }
184
+
185
+ getCharLastIndex(index: number): number {
186
+ return this.getCharEndIndex(index) - 1 ?? 0;
187
+ }
188
+
149
189
  }
@@ -48,7 +48,7 @@ describe("Literal", () => {
48
48
  expect(result).toEqual(null);
49
49
  expect(cursor.index).toBe(10);
50
50
  expect(cursor.error?.startIndex).toBe(0);
51
- expect(cursor.error?.lastIndex).toBe(11);
51
+ expect(cursor.error?.lastIndex).toBe(10);
52
52
  expect(cursor.error?.pattern).toBe(literal);
53
53
  });
54
54
 
@@ -163,4 +163,44 @@ describe("Literal", () => {
163
163
 
164
164
  expect(pattern).toBeNull();
165
165
  });
166
+
167
+ test("Unicode", () => {
168
+ const literal = new Literal("a", "🔴");
169
+ const cursor = new Cursor("🔴");
170
+ const result = literal.parse(cursor);
171
+ expect(result).not.toBeNull();
172
+ expect(cursor.index).toBe(0);
173
+ expect(result?.toString()).toBe("🔴");
174
+ expect(cursor.error).toBeNull();
175
+ });
176
+
177
+ test("Unicode In Middle", () => {
178
+ const literal = new Literal("a", "|🔴|");
179
+ const cursor = new Cursor("|🔴|");
180
+ const result = literal.parse(cursor);
181
+ expect(result).not.toBeNull();
182
+ expect(cursor.index).toBe(3);
183
+ expect(result?.toString()).toBe("|🔴|");
184
+ expect(cursor.error).toBeNull();
185
+ });
186
+
187
+ test("Unicode At the End", () => {
188
+ const literal = new Literal("a", "|🔴");
189
+ const cursor = new Cursor("|🔴");
190
+ const result = literal.parse(cursor);
191
+ expect(result).not.toBeNull();
192
+ expect(cursor.index).toBe(1);
193
+ expect(result?.toString()).toBe("|🔴");
194
+ expect(cursor.error).toBeNull();
195
+ });
196
+
197
+ test("Unicode At the Beginning", () => {
198
+ const literal = new Literal("a", "🔴|");
199
+ const cursor = new Cursor("🔴|");
200
+ const result = literal.parse(cursor);
201
+ expect(result).not.toBeNull();
202
+ expect(cursor.index).toBe(2);
203
+ expect(result?.toString()).toBe("🔴|");
204
+ expect(cursor.error).toBeNull();
205
+ });
166
206
  });