@thomasfosterau/effect-parse 0.0.0-stage → 0.1.0

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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +550 -2
  3. package/dist/Ascii.d.ts +392 -0
  4. package/dist/Ascii.d.ts.map +1 -0
  5. package/dist/Ascii.js +610 -0
  6. package/dist/Ascii.js.map +1 -0
  7. package/dist/Binary.d.ts +153 -0
  8. package/dist/Binary.d.ts.map +1 -0
  9. package/dist/Binary.js +171 -0
  10. package/dist/Binary.js.map +1 -0
  11. package/dist/Bits.d.ts +60 -0
  12. package/dist/Bits.d.ts.map +1 -0
  13. package/dist/Bits.js +144 -0
  14. package/dist/Bits.js.map +1 -0
  15. package/dist/Encoding.d.ts +116 -0
  16. package/dist/Encoding.d.ts.map +1 -0
  17. package/dist/Encoding.js +85 -0
  18. package/dist/Encoding.js.map +1 -0
  19. package/dist/Expression.d.ts +124 -0
  20. package/dist/Expression.d.ts.map +1 -0
  21. package/dist/Expression.js +139 -0
  22. package/dist/Expression.js.map +1 -0
  23. package/dist/Input.d.ts +301 -0
  24. package/dist/Input.d.ts.map +1 -0
  25. package/dist/Input.js +312 -0
  26. package/dist/Input.js.map +1 -0
  27. package/dist/Nom.d.ts +518 -0
  28. package/dist/Nom.d.ts.map +1 -0
  29. package/dist/Nom.js +519 -0
  30. package/dist/Nom.js.map +1 -0
  31. package/dist/ParseError.d.ts +120 -0
  32. package/dist/ParseError.d.ts.map +1 -0
  33. package/dist/ParseError.js +301 -0
  34. package/dist/ParseError.js.map +1 -0
  35. package/dist/Parser.d.ts +1312 -0
  36. package/dist/Parser.d.ts.map +1 -0
  37. package/dist/Parser.js +1936 -0
  38. package/dist/Parser.js.map +1 -0
  39. package/dist/ParserSchema.d.ts +31 -0
  40. package/dist/ParserSchema.d.ts.map +1 -0
  41. package/dist/ParserSchema.js +56 -0
  42. package/dist/ParserSchema.js.map +1 -0
  43. package/dist/ParserStream.d.ts +127 -0
  44. package/dist/ParserStream.d.ts.map +1 -0
  45. package/dist/ParserStream.js +270 -0
  46. package/dist/ParserStream.js.map +1 -0
  47. package/dist/Text.d.ts +330 -0
  48. package/dist/Text.d.ts.map +1 -0
  49. package/dist/Text.js +529 -0
  50. package/dist/Text.js.map +1 -0
  51. package/dist/Token.d.ts +225 -0
  52. package/dist/Token.d.ts.map +1 -0
  53. package/dist/Token.js +294 -0
  54. package/dist/Token.js.map +1 -0
  55. package/dist/index.d.ts +17 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +17 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/internal/compiler/closure.d.ts +25 -0
  60. package/dist/internal/compiler/closure.d.ts.map +1 -0
  61. package/dist/internal/compiler/closure.js +1668 -0
  62. package/dist/internal/compiler/closure.js.map +1 -0
  63. package/dist/internal/compiler/entries.d.ts +19 -0
  64. package/dist/internal/compiler/entries.d.ts.map +1 -0
  65. package/dist/internal/compiler/entries.js +32 -0
  66. package/dist/internal/compiler/entries.js.map +1 -0
  67. package/dist/internal/compiler/first.d.ts +33 -0
  68. package/dist/internal/compiler/first.d.ts.map +1 -0
  69. package/dist/internal/compiler/first.js +235 -0
  70. package/dist/internal/compiler/first.js.map +1 -0
  71. package/dist/internal/compiler/jit.d.ts +20 -0
  72. package/dist/internal/compiler/jit.d.ts.map +1 -0
  73. package/dist/internal/compiler/jit.js +1030 -0
  74. package/dist/internal/compiler/jit.js.map +1 -0
  75. package/dist/internal/compiler/registry.d.ts +34 -0
  76. package/dist/internal/compiler/registry.d.ts.map +1 -0
  77. package/dist/internal/compiler/registry.js +46 -0
  78. package/dist/internal/compiler/registry.js.map +1 -0
  79. package/dist/internal/compiler/sets.d.ts +29 -0
  80. package/dist/internal/compiler/sets.d.ts.map +1 -0
  81. package/dist/internal/compiler/sets.js +85 -0
  82. package/dist/internal/compiler/sets.js.map +1 -0
  83. package/dist/internal/entries.d.ts +27 -0
  84. package/dist/internal/entries.d.ts.map +1 -0
  85. package/dist/internal/entries.js +42 -0
  86. package/dist/internal/entries.js.map +1 -0
  87. package/dist/internal/node.d.ts +251 -0
  88. package/dist/internal/node.d.ts.map +1 -0
  89. package/dist/internal/node.js +73 -0
  90. package/dist/internal/node.js.map +1 -0
  91. package/dist/internal/numbers.d.ts +41 -0
  92. package/dist/internal/numbers.d.ts.map +1 -0
  93. package/dist/internal/numbers.js +302 -0
  94. package/dist/internal/numbers.js.map +1 -0
  95. package/dist/internal/resume.d.ts +39 -0
  96. package/dist/internal/resume.d.ts.map +1 -0
  97. package/dist/internal/resume.js +37 -0
  98. package/dist/internal/resume.js.map +1 -0
  99. package/dist/internal/text.d.ts +72 -0
  100. package/dist/internal/text.d.ts.map +1 -0
  101. package/dist/internal/text.js +391 -0
  102. package/dist/internal/text.js.map +1 -0
  103. package/dist/internal/tokens.d.ts +93 -0
  104. package/dist/internal/tokens.d.ts.map +1 -0
  105. package/dist/internal/tokens.js +400 -0
  106. package/dist/internal/tokens.js.map +1 -0
  107. package/package.json +115 -3
  108. package/src/Ascii.ts +741 -0
  109. package/src/Binary.ts +228 -0
  110. package/src/Bits.ts +192 -0
  111. package/src/Encoding.ts +128 -0
  112. package/src/Expression.ts +245 -0
  113. package/src/Input.ts +429 -0
  114. package/src/Nom.ts +642 -0
  115. package/src/ParseError.ts +378 -0
  116. package/src/Parser.ts +2763 -0
  117. package/src/ParserSchema.ts +87 -0
  118. package/src/ParserStream.ts +390 -0
  119. package/src/Text.ts +757 -0
  120. package/src/Token.ts +416 -0
  121. package/src/index.ts +16 -0
  122. package/src/internal/compiler/closure.ts +1583 -0
  123. package/src/internal/compiler/entries.ts +36 -0
  124. package/src/internal/compiler/first.ts +252 -0
  125. package/src/internal/compiler/jit.ts +1091 -0
  126. package/src/internal/compiler/registry.ts +79 -0
  127. package/src/internal/compiler/sets.ts +100 -0
  128. package/src/internal/entries.ts +51 -0
  129. package/src/internal/node.ts +207 -0
  130. package/src/internal/numbers.ts +291 -0
  131. package/src/internal/resume.ts +55 -0
  132. package/src/internal/text.ts +411 -0
  133. package/src/internal/tokens.ts +409 -0
package/src/Text.ts ADDED
@@ -0,0 +1,757 @@
1
+ /**
2
+ * Unicode text parsers, generic over the input's encoding.
3
+ *
4
+ * `Text.make(encoding)` builds one set of parsers ({@link TextParsers}) for
5
+ * an {@link Encoding.Encoding}, and ready-made sets are provided for
6
+ * JavaScript strings and the common byte encodings:
7
+ *
8
+ * | Parsers | Input | Offsets |
9
+ * |---|---|---|
10
+ * | `Text.string` | `string` | UTF-16 code units |
11
+ * | `Text.utf8` | `Uint8Array` of UTF-8 | bytes |
12
+ * | `Text.utf16le`, `Text.utf16be` | `Uint8Array` of UTF-16 | bytes |
13
+ * | `Text.latin1` | `Uint8Array` of ISO-8859-1 | bytes |
14
+ *
15
+ * Every set has the same members, and every parser returns `string`s, so a
16
+ * grammar written against `TextParsers<I>` parses all of them:
17
+ *
18
+ * ```ts
19
+ * import { Parser, Text } from "@thomasfosterau/effect-parse"
20
+ *
21
+ * const greeting = <I extends string | Uint8Array>(T: Text.TextParsers<I>) =>
22
+ * Parser.preceded(T.literal("¡hola "), T.identifier)
23
+ *
24
+ * Parser.parse(greeting(Text.string), "¡hola mundo") // "mundo"
25
+ * Parser.parse(greeting(Text.utf8), new TextEncoder().encode("¡hola Zoë")) // "Zoë"
26
+ * ```
27
+ *
28
+ * Tokens are Unicode code points, and character classes follow Unicode
29
+ * properties: `alpha1` matches `"Ωμέγα"`, `space1` matches a no-break space.
30
+ * Use the `Ascii` module for the ASCII-only classes.
31
+ *
32
+ * Decoding is strict. Malformed input (invalid UTF-8, unpaired surrogates)
33
+ * never matches, and error messages point at it. On partial input, a
34
+ * character split across chunks reports `Incomplete`, so byte streams can be
35
+ * parsed as text without decoding them first.
36
+ *
37
+ * @since 0.1.0
38
+ */
39
+ import type { Encoding } from "./Encoding.js"
40
+ import * as Encodings from "./Encoding.js"
41
+ import type { Input, State } from "./Input.js"
42
+ import { Fail } from "./Input.js"
43
+ import { escapedImpl, overflow, takeEscapedImpl, toFloat } from "./Ascii.js"
44
+ import { codePointPredicate, compile, indexOf, matchLiteral } from "./internal/tokens.js"
45
+ import { floatText as markFloatText } from "./internal/numbers.js"
46
+ import * as internal from "./internal/text.js"
47
+ import type { Node } from "./internal/node.js"
48
+ import type { ErrorKind } from "./ParseError.js"
49
+ import type { Parser, Range } from "./Parser.js"
50
+ import * as P from "./Parser.js"
51
+ import type { StringTokenSet } from "./Token.js"
52
+
53
+ // -----------------------------------------------------------------------------
54
+ // Unicode predicates
55
+ // -----------------------------------------------------------------------------
56
+
57
+ /**
58
+ * A predicate over characters. It accepts a one-character string or a code
59
+ * point.
60
+ *
61
+ * @since 0.1.0
62
+ * @category models
63
+ */
64
+ export type CharPredicate = (char: string | number) => boolean
65
+
66
+ const toCodePoint = (c: string | number): number => typeof c === "number" ? c : c.codePointAt(0) ?? -1
67
+
68
+ const makePredicate = (name: string, re: RegExp): CharPredicate => {
69
+ // Results for the Basic Multilingual Plane are cached: a table lookup is
70
+ // much faster than a regular expression test.
71
+ let ascii: Uint8Array | undefined
72
+ let bmp: Uint8Array | undefined
73
+ return codePointPredicate(name, (c: string | number): boolean => {
74
+ const x = toCodePoint(c)
75
+ if (x < 0) return false
76
+ let table: Uint8Array
77
+ if (x < 128) table = ascii ??= new Uint8Array(128)
78
+ else if (x < 0x10000) table = bmp ??= new Uint8Array(0x10000)
79
+ else return re.test(String.fromCodePoint(x))
80
+ let v = table[x]!
81
+ if (v === 0) table[x] = v = re.test(String.fromCharCode(x)) ? 2 : 1
82
+ return v === 2
83
+ })
84
+ }
85
+
86
+ /**
87
+ * A predicate testing a Unicode property, using the syntax of regular
88
+ * expression property escapes (`\p{...}`): a general category (`"Lu"`,
89
+ * `"Letter"`), a binary property (`"Emoji"`, `"Dash"`) or a script
90
+ * (`"Script=Greek"`).
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * import { Text } from "@thomasfosterau/effect-parse"
95
+ *
96
+ * const greek = Text.string.takeWhile(Text.property("Script=Greek"), { min: 1 })
97
+ * ```
98
+ *
99
+ * @since 0.1.0
100
+ * @category predicates
101
+ */
102
+ export const property = (name: string): CharPredicate => makePredicate(name, new RegExp(`^\\p{${name}}$`, "u"))
103
+
104
+ /**
105
+ * Letters and other alphabetic characters (the Unicode `Alphabetic`
106
+ * property, Rust's `char::is_alphabetic`).
107
+ *
108
+ * @since 0.1.0
109
+ * @category predicates
110
+ */
111
+ export const isAlphabetic: CharPredicate = makePredicate("isAlphabetic", /^\p{Alphabetic}$/u)
112
+
113
+ /**
114
+ * Letters (general category `L`).
115
+ *
116
+ * @since 0.1.0
117
+ * @category predicates
118
+ */
119
+ export const isLetter: CharPredicate = makePredicate("isLetter", /^\p{L}$/u)
120
+
121
+ /**
122
+ * Uppercase characters (the Unicode `Uppercase` property).
123
+ *
124
+ * @since 0.1.0
125
+ * @category predicates
126
+ */
127
+ export const isUppercase: CharPredicate = makePredicate("isUppercase", /^\p{Uppercase}$/u)
128
+
129
+ /**
130
+ * Lowercase characters (the Unicode `Lowercase` property).
131
+ *
132
+ * @since 0.1.0
133
+ * @category predicates
134
+ */
135
+ export const isLowercase: CharPredicate = makePredicate("isLowercase", /^\p{Lowercase}$/u)
136
+
137
+ /**
138
+ * Decimal digits of any script (general category `Nd`): `0`–`9`, `٠`–`٩`,
139
+ * `०`–`९`, ...
140
+ *
141
+ * @since 0.1.0
142
+ * @category predicates
143
+ */
144
+ export const isDecimalDigit: CharPredicate = makePredicate("isDecimalDigit", /^\p{Nd}$/u)
145
+
146
+ /**
147
+ * Numeric characters (general category `N`), including `½` and `Ⅻ` (Rust's
148
+ * `char::is_numeric`).
149
+ *
150
+ * @since 0.1.0
151
+ * @category predicates
152
+ */
153
+ export const isNumeric: CharPredicate = makePredicate("isNumeric", /^\p{N}$/u)
154
+
155
+ /**
156
+ * Alphabetic or numeric characters (Rust's `char::is_alphanumeric`).
157
+ *
158
+ * @since 0.1.0
159
+ * @category predicates
160
+ */
161
+ export const isAlphanumeric: CharPredicate = makePredicate("isAlphanumeric", /^[\p{Alphabetic}\p{N}]$/u)
162
+
163
+ /**
164
+ * Whitespace, including line breaks (the Unicode `White_Space` property,
165
+ * Rust's `char::is_whitespace`).
166
+ *
167
+ * @since 0.1.0
168
+ * @category predicates
169
+ */
170
+ export const isWhitespace: CharPredicate = makePredicate("isWhitespace", /^\p{White_Space}$/u)
171
+
172
+ /**
173
+ * Whitespace that does not break lines: `White_Space` except `\n`, `\v`,
174
+ * `\f`, `\r`, U+0085, U+2028 and U+2029. Includes tabs and no-break spaces.
175
+ *
176
+ * @since 0.1.0
177
+ * @category predicates
178
+ */
179
+ export const isSpace: CharPredicate = makePredicate(
180
+ "isSpace",
181
+ /^(?![\n\v\f\r\u0085\u2028\u2029])\p{White_Space}$/u
182
+ )
183
+
184
+ /**
185
+ * Characters that can start an identifier (the Unicode `ID_Start` property,
186
+ * see UAX #31).
187
+ *
188
+ * @since 0.1.0
189
+ * @category predicates
190
+ */
191
+ export const isIdStart: CharPredicate = makePredicate("isIdStart", /^\p{ID_Start}$/u)
192
+
193
+ /**
194
+ * Characters that can continue an identifier (the Unicode `ID_Continue`
195
+ * property, see UAX #31).
196
+ *
197
+ * @since 0.1.0
198
+ * @category predicates
199
+ */
200
+ export const isIdContinue: CharPredicate = makePredicate("isIdContinue", /^\p{ID_Continue}$/u)
201
+
202
+ /**
203
+ * Punctuation (general category `P`).
204
+ *
205
+ * @since 0.1.0
206
+ * @category predicates
207
+ */
208
+ export const isPunctuation: CharPredicate = makePredicate("isPunctuation", /^\p{P}$/u)
209
+
210
+ /**
211
+ * Control characters (general category `Cc`).
212
+ *
213
+ * @since 0.1.0
214
+ * @category predicates
215
+ */
216
+ export const isControl: CharPredicate = makePredicate("isControl", /^\p{Cc}$/u)
217
+
218
+ // -----------------------------------------------------------------------------
219
+ // Parsers
220
+ // -----------------------------------------------------------------------------
221
+
222
+ /**
223
+ * The text parsers for one encoding. Build a set with {@link make}, or use
224
+ * {@link string}, {@link utf8}, {@link utf16le}, {@link utf16be} or
225
+ * {@link latin1}.
226
+ *
227
+ * Every parser returns text as a `string`, whatever the input. To keep a
228
+ * slice of a byte input as bytes instead, use `Parser.taken`.
229
+ *
230
+ * @since 0.1.0
231
+ * @category models
232
+ */
233
+ export interface TextParsers<I extends string | Uint8Array> {
234
+ /** The encoding these parsers read. */
235
+ readonly encoding: Encoding<I>
236
+
237
+ // Tokens
238
+
239
+ /** Any single character (code point). */
240
+ readonly any: Parser<string, never, never, I>
241
+ /**
242
+ * A literal string, returned as written. On byte inputs it is matched in
243
+ * its encoded form.
244
+ */
245
+ readonly literal: <const L extends string>(literal: L) => Parser<L, never, never, I>
246
+ /**
247
+ * A literal string, ignoring case (by simple Unicode case folding, so
248
+ * `"straße"` matches `"STRAßE"` but not `"STRASSE"`). Returns the input as
249
+ * written.
250
+ */
251
+ readonly caseless: (literal: string) => Parser<string, never, never, I>
252
+ /** One character belonging to a set (the characters of a string, a range, a predicate, or an array of these). */
253
+ readonly oneOf: (set: StringTokenSet) => Parser<string, never, never, I>
254
+ /** One character *not* belonging to a set. */
255
+ readonly noneOf: (set: StringTokenSet) => Parser<string, never, never, I>
256
+ /** An optional byte order mark, U+FEFF. Returns whether one was present. */
257
+ readonly bom: Parser<boolean, never, never, I>
258
+
259
+ // Slices
260
+
261
+ /** Exactly `count` characters. */
262
+ readonly take: (count: number) => Parser<string, never, never, I>
263
+ /** The longest run of characters belonging to a set, with a count in `range` (default: any). */
264
+ readonly takeWhile: (set: StringTokenSet, range?: Range) => Parser<string, never, never, I>
265
+ /** The longest run of characters *not* belonging to a set, with a count in `range`. */
266
+ readonly takeTill: (set: StringTokenSet, range?: Range) => Parser<string, never, never, I>
267
+ /** Characters up to (not including) the first occurrence of a literal. */
268
+ readonly takeUntil: (literal: string, range?: Range) => Parser<string, never, never, I>
269
+ /** The rest of the input. */
270
+ readonly rest: Parser<string, never, never, I>
271
+ /**
272
+ * Runs a parser and returns the text it consumed (`Parser.taken`, decoded
273
+ * to a `string`).
274
+ */
275
+ readonly taken: <A, E, R>(self: Parser<A, E, R, I>) => Parser<string, E, R, I>
276
+
277
+ // Character classes
278
+
279
+ /** Zero or more alphabetic characters ({@link isAlphabetic}). */
280
+ readonly alpha0: Parser<string, never, never, I>
281
+ /** One or more alphabetic characters ({@link isAlphabetic}). */
282
+ readonly alpha1: Parser<string, never, never, I>
283
+ /** Zero or more decimal digits of any script ({@link isDecimalDigit}). */
284
+ readonly digit0: Parser<string, never, never, I>
285
+ /** One or more decimal digits of any script ({@link isDecimalDigit}). */
286
+ readonly digit1: Parser<string, never, never, I>
287
+ /** Zero or more alphanumeric characters ({@link isAlphanumeric}). */
288
+ readonly alphanumeric0: Parser<string, never, never, I>
289
+ /** One or more alphanumeric characters ({@link isAlphanumeric}). */
290
+ readonly alphanumeric1: Parser<string, never, never, I>
291
+ /** Zero or more non-breaking whitespace characters ({@link isSpace}). */
292
+ readonly space0: Parser<string, never, never, I>
293
+ /** One or more non-breaking whitespace characters ({@link isSpace}). */
294
+ readonly space1: Parser<string, never, never, I>
295
+ /** Zero or more whitespace characters, including line breaks ({@link isWhitespace}). */
296
+ readonly multispace0: Parser<string, never, never, I>
297
+ /** One or more whitespace characters, including line breaks ({@link isWhitespace}). */
298
+ readonly multispace1: Parser<string, never, never, I>
299
+ /**
300
+ * An identifier: an {@link isIdStart} character or `_`, then any
301
+ * {@link isIdContinue} characters (UAX #31 default identifiers).
302
+ */
303
+ readonly identifier: Parser<string, never, never, I>
304
+
305
+ // Line endings
306
+
307
+ /** `\n` */
308
+ readonly newline: Parser<"\n", never, never, I>
309
+ /** `\r\n` */
310
+ readonly crlf: Parser<"\r\n", never, never, I>
311
+ /** `\t` */
312
+ readonly tab: Parser<"\t", never, never, I>
313
+ /** `\n` or `\r\n`. */
314
+ readonly lineEnding: Parser<"\n" | "\r\n", never, never, I>
315
+ /** Everything up to (not including) a line ending. A `\r` not followed by `\n` is an error. */
316
+ readonly tillLineEnding: Parser<string, never, never, I>
317
+
318
+ // Numbers (ASCII digits, as in most languages)
319
+
320
+ /** An unsigned decimal integer of ASCII digits. Fails if it is not a safe integer. */
321
+ readonly decUint: Parser<number, string, never, I>
322
+ /** A signed decimal integer of ASCII digits. Fails if it is not a safe integer. */
323
+ readonly decInt: Parser<number, string, never, I>
324
+ /** An unsigned decimal integer of ASCII digits, as a `bigint`. */
325
+ readonly decBigUint: Parser<bigint, never, never, I>
326
+ /** A signed decimal integer of ASCII digits, as a `bigint`. */
327
+ readonly decBigInt: Parser<bigint, never, never, I>
328
+ /** A hexadecimal unsigned integer, without prefix. Fails if it is not a safe integer. */
329
+ readonly hexUint: Parser<number, string, never, I>
330
+ /** A floating point number, as accepted by `Ascii.float`. */
331
+ readonly float: Parser<number, never, never, I>
332
+
333
+ // Escapes
334
+
335
+ /**
336
+ * Recognizes text containing escape sequences, returning it raw (see
337
+ * `Ascii.takeEscaped`). `control` is the escape character.
338
+ */
339
+ readonly takeEscaped: <A, E, R, B, E2, R2>(
340
+ normal: Parser<A, E, R, I>,
341
+ control: string,
342
+ escapable: Parser<B, E2, R2, I>
343
+ ) => Parser<string, E | E2, R | R2, I>
344
+ /**
345
+ * Parses text containing escape sequences, returning it unescaped (see
346
+ * `Ascii.escaped`). `control` is the escape character.
347
+ */
348
+ readonly escaped: <E, R, E2, R2>(
349
+ normal: Parser<string, E, R, I>,
350
+ control: string,
351
+ escape: Parser<string, E2, R2, I>
352
+ ) => Parser<string, E | E2, R | R2, I>
353
+ }
354
+
355
+ const normalize = (r: Range | undefined): [number, number] => {
356
+ if (r === undefined) return [0, Infinity]
357
+ if (typeof r === "number") return [r, r]
358
+ return [r.min ?? 0, r.max ?? Infinity]
359
+ }
360
+
361
+ const isAsciiDigit = codePointPredicate("isAsciiDigit", (c: number): boolean => c >= 48 && c <= 57, [[48, 57]])
362
+ const isAsciiHexDigit = codePointPredicate(
363
+ "isAsciiHexDigit",
364
+ (c: number): boolean => (c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102),
365
+ [[48, 57], [65, 70], [97, 102]]
366
+ )
367
+ const isLineBreak = (c: number): boolean => c === 10 || c === 13
368
+
369
+ const expectedAny = ["any character"]
370
+ const expectedLineEnding = ["line ending"]
371
+ const expectedIdentifier = ["identifier"]
372
+
373
+ /**
374
+ * Builds the text parsers for an encoding.
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * import { Encoding, Parser, Text } from "@thomasfosterau/effect-parse"
379
+ *
380
+ * const T = Text.make(Encoding.latin1)
381
+ * Parser.parse(T.alpha1, new Uint8Array([0x63, 0x61, 0x66, 0xe9])) // "café"
382
+ * ```
383
+ *
384
+ * @since 0.1.0
385
+ * @category constructors
386
+ */
387
+ export const make = <I extends string | Uint8Array>(encoding: Encoding<I>): TextParsers<I> => {
388
+ const enc = encoding as Encoding<any>
389
+ const fast = internal.fastLimit(enc)
390
+
391
+ // Each parser records its encoding on byte input (`state.encoding`), so
392
+ // that errors can show the input as text.
393
+ const backtrack = (state: State<any>, kind: ErrorKind, expected: ReadonlyArray<string>, offset = state.pos): Fail =>
394
+ state.backtrack(kind, expected, offset)
395
+
396
+ /** The text `[start, end)` after checking it is well formed, or `Fail`. */
397
+ const textOf = (
398
+ state: State<any>,
399
+ start: number,
400
+ end: number,
401
+ kind: ErrorKind,
402
+ expected: ReadonlyArray<string>
403
+ ): string | Fail => {
404
+ const count = internal.validate(enc, state, start, end)
405
+ if (count < 0) return backtrack(state, kind, expected, -1 - count)
406
+ return enc.text(state.input, start, end)
407
+ }
408
+
409
+ const takeWhileText = (
410
+ test: (cp: number) => boolean,
411
+ r: Range | undefined,
412
+ negate: boolean,
413
+ name: string,
414
+ expected: ReadonlyArray<string>,
415
+ kind: ErrorKind,
416
+ hint: boolean,
417
+ set: unknown = test
418
+ ): Parser<string, never, never, I> => {
419
+ const [min, max] = normalize(r)
420
+ return P.described<string, never, never, I>((state: State<any>) => {
421
+ if (state.inputKind === 1) state.encoding = enc
422
+ const start = state.pos
423
+ internal.scan(enc, state, test, negate, max, state.pos, fast)
424
+ const offset = internal.scanOffset
425
+ const count = internal.scanCount
426
+ const stop = internal.scanStop
427
+ if (state.partial && (stop === internal.End || stop === internal.Truncated)) return state.incomplete(1, expected)
428
+ if (count < min) return backtrack(state, kind, expected, offset)
429
+ // More matching characters would have been accepted where we stopped
430
+ if (hint && count < max) state.setHint(offset, expected)
431
+ state.pos = offset
432
+ return enc.text(state.input, start, offset)
433
+ }, name, { _tag: "TakeWhile", set, negate, min, max, encoding: enc })
434
+ }
435
+
436
+ const any: Parser<string, never, never, I> = P.described<string, never, never, I>((state: State<any>) => {
437
+ if (state.inputKind === 1) state.encoding = enc
438
+ const cp = internal.next(enc, state)
439
+ if (cp < 0) return internal.failAt(state, cp, "Token", expectedAny)
440
+ state.pos += internal.width
441
+ return internal.charOf(cp)
442
+ }, "any", { _tag: "Any", encoding: enc })
443
+
444
+ const literal = <const L extends string>(lit: L): Parser<L, never, never, I> => {
445
+ const encoded = enc.encode(lit)
446
+ const len = encoded.length
447
+ const expected = [JSON.stringify(lit)]
448
+ const node: Node = { _tag: "Literal", literal: lit, caseless: false, encoding: enc }
449
+ if (len === 1) {
450
+ // Single unit literals (punctuation, mostly) are compared directly
451
+ const unit = typeof encoded === "string" ? encoded.charCodeAt(0) : encoded[0]!
452
+ return P.described<L, never, never, I>((state: State<any>) => {
453
+ const pos = state.pos
454
+ if (state.inputKind === 1) state.encoding = enc
455
+ if (pos >= state.end) return state.eoi("Literal", expected, 1)
456
+ const input = state.input
457
+ if ((state.inputKind === 0 ? (input as string).charCodeAt(pos) : (input as Uint8Array)[pos]) !== unit) {
458
+ return backtrack(state, "Literal", expected)
459
+ }
460
+ state.pos = pos + 1
461
+ return lit
462
+ }, expected[0], node)
463
+ }
464
+ return P.described<L, never, never, I>((state: State<any>) => {
465
+ if (state.inputKind === 1) state.encoding = enc
466
+ const r = matchLiteral(state, encoded, false)
467
+ if (r === -1) return backtrack(state, "Literal", expected)
468
+ if (r === -2) {
469
+ if (state.partial) return state.incomplete(len - (state.end - state.pos), expected)
470
+ return backtrack(state, "Literal", expected)
471
+ }
472
+ state.pos += len
473
+ return lit
474
+ }, expected[0], node)
475
+ }
476
+
477
+ const caseless = (lit: string): Parser<string, never, never, I> => {
478
+ const folded = Array.from(lit, (c) => internal.fold(c.codePointAt(0)!))
479
+ const expected = [`${JSON.stringify(lit)} (case insensitive)`]
480
+ return P.described<string, never, never, I>((state: State<any>) => {
481
+ if (state.inputKind === 1) state.encoding = enc
482
+ const start = state.pos
483
+ let pos = start
484
+ for (let i = 0; i < folded.length; i++) {
485
+ const cp = internal.next(enc, state, pos)
486
+ if (cp === internal.End || cp === internal.Truncated) {
487
+ return state.partial ? state.incomplete(1, expected) : backtrack(state, "Literal", expected)
488
+ }
489
+ if (cp < 0 || internal.fold(cp) !== folded[i]) return backtrack(state, "Literal", expected)
490
+ pos += internal.width
491
+ }
492
+ state.pos = pos
493
+ return enc.text(state.input, start, pos)
494
+ }, expected[0], { _tag: "Literal", literal: lit, caseless: true, encoding: enc })
495
+ }
496
+
497
+ const oneOfImpl = (set: StringTokenSet, negate: boolean): Parser<string, never, never, I> => {
498
+ const m = compile(set, "string")
499
+ const test = m.code
500
+ const expected = [negate ? `anything but ${m.description}` : m.description]
501
+ const kind: ErrorKind = negate ? "NoneOf" : "OneOf"
502
+ return P.described<string, never, never, I>((state: State<any>) => {
503
+ if (state.inputKind === 1) state.encoding = enc
504
+ const cp = internal.next(enc, state)
505
+ if (cp < 0) return internal.failAt(state, cp, kind, expected)
506
+ const w = internal.width
507
+ if (test(cp) === negate) return state.backtrack(kind, expected)
508
+ state.pos += w
509
+ return internal.charOf(cp)
510
+ }, expected[0], { _tag: "OneOf", set, negate, encoding: enc })
511
+ }
512
+
513
+ const take = (count: number): Parser<string, never, never, I> => {
514
+ const expected = [count === 1 ? "1 character" : `${count} characters`]
515
+ return P.described<string, never, never, I>((state: State<any>) => {
516
+ if (state.inputKind === 1) state.encoding = enc
517
+ const start = state.pos
518
+ let pos = start
519
+ for (let got = 0; got < count; got++) {
520
+ const cp = internal.next(enc, state, pos)
521
+ if (cp === internal.End) return state.eoi("Take", expected, count - got)
522
+ if (cp < 0) return internal.failAt(state, cp, "Take", expected, pos)
523
+ pos += internal.width
524
+ }
525
+ state.pos = pos
526
+ return enc.text(state.input, start, pos)
527
+ }, "take", { _tag: "Take", count, encoding: enc })
528
+ }
529
+
530
+ const takeUntil = (lit: string, r?: Range): Parser<string, never, never, I> => {
531
+ const [min, max] = normalize(r)
532
+ const needle = enc.encode(lit)
533
+ const unit = enc.unit
534
+ const expected = [JSON.stringify(lit)]
535
+ return P.described<string, never, never, I>((state: State<any>) => {
536
+ if (state.inputKind === 1) state.encoding = enc
537
+ const start = state.pos
538
+ let idx = indexOf(state, needle as Input, start)
539
+ // Multi-unit encodings: only accept matches on a character boundary
540
+ while (unit > 1 && idx >= 0 && (idx - start) % unit !== 0) idx = indexOf(state, needle as Input, idx + 1)
541
+ if (idx < 0) {
542
+ if (state.partial) return state.incomplete(0, expected)
543
+ return backtrack(state, "TakeUntil", expected, state.end)
544
+ }
545
+ const count = internal.validate(enc, state, start, idx)
546
+ if (count < 0) return backtrack(state, "TakeUntil", expected, -1 - count)
547
+ // As in winnow, the first occurrence must lie within the range
548
+ if (count < min) return backtrack(state, "TakeUntil", expected, idx)
549
+ if (count > max) return backtrack(state, "TakeUntil", expected)
550
+ state.pos = idx
551
+ return enc.text(state.input, start, idx)
552
+ }, "takeUntil", { _tag: "TakeUntil", literal: lit, min, max, encoding: enc })
553
+ }
554
+
555
+ const always = () => true
556
+ const rest: Parser<string, never, never, I> = P.described<string, never, never, I>((state: State<any>) => {
557
+ if (state.inputKind === 1) state.encoding = enc
558
+ const start = state.pos
559
+ internal.scan(enc, state, always, false, Infinity, state.pos, fast)
560
+ const offset = internal.scanOffset
561
+ if (internal.scanStop === internal.Truncated) return state.incomplete(1, expectedAny)
562
+ if (internal.scanStop === internal.Invalid) return backtrack(state, "Token", expectedAny, offset)
563
+ state.pos = offset
564
+ return enc.text(state.input, start, offset)
565
+ }, "rest", { _tag: "Rest", encoding: enc })
566
+
567
+ const taken = <A, E, R>(self: Parser<A, E, R, I>): Parser<string, E, R, I> => {
568
+ const step = self.step
569
+ return P.described<string, E, R, I>((state: State<any>) => {
570
+ if (state.inputKind === 1) state.encoding = enc
571
+ const start = state.pos
572
+ if (step(state) === Fail) return Fail
573
+ return textOf(state, start, state.pos, "Slice", expectedAny)
574
+ }, self.name, { _tag: "Taken", self, encoding: enc })
575
+ }
576
+
577
+ const cls = (test: CharPredicate, min: 0 | 1, description: string, kind: ErrorKind, hint = true) =>
578
+ takeWhileText(test as any, { min }, false, `${kind.toLowerCase()}${min}`, [description], kind, hint)
579
+
580
+ const identifierStart = (cp: number) => cp === 95 || isIdStart(cp)
581
+ const identifier: Parser<string, never, never, I> = P.described<string, never, never, I>((state: State<any>) => {
582
+ if (state.inputKind === 1) state.encoding = enc
583
+ const start = state.pos
584
+ const cp = internal.next(enc, state)
585
+ if (cp < 0) return internal.failAt(state, cp, "TakeWhile", expectedIdentifier)
586
+ if (!identifierStart(cp)) return state.backtrack("TakeWhile", expectedIdentifier)
587
+ internal.scan(enc, state, isIdContinue as any, false, Infinity, start + internal.width, fast)
588
+ const stop = internal.scanStop
589
+ if (state.partial && (stop === internal.End || stop === internal.Truncated)) {
590
+ return state.incomplete(1, expectedIdentifier)
591
+ }
592
+ state.pos = internal.scanOffset
593
+ return enc.text(state.input, start, state.pos)
594
+ }, "identifier", { _tag: "Identifier", encoding: enc })
595
+
596
+ // Decoded rather than matched as a literal, as not every encoding can encode it
597
+ const bom: Parser<boolean, never, never, I> = P.described<boolean, never, never, I>((state: State<any>) => {
598
+ if (state.inputKind === 1) state.encoding = enc
599
+ const cp = internal.next(enc, state)
600
+ if (cp === 0xfeff) {
601
+ state.pos += internal.width
602
+ return true
603
+ }
604
+ if (cp === internal.Truncated || (cp === internal.End && state.partial)) {
605
+ return state.incomplete(1, ["byte order mark"])
606
+ }
607
+ return false
608
+ }, "bom", { _tag: "Bom", encoding: enc })
609
+
610
+ const newline = literal("\n")
611
+ const crlf = literal("\r\n")
612
+
613
+ const tillLineEnding: Parser<string, never, never, I> = P.described<string, never, never, I>((state: State<any>) => {
614
+ if (state.inputKind === 1) state.encoding = enc
615
+ const start = state.pos
616
+ internal.scan(enc, state, isLineBreak, true, Infinity, state.pos, fast)
617
+ const offset = internal.scanOffset
618
+ const stop = internal.scanStop
619
+ if (stop === internal.Truncated || (stop === internal.End && state.partial)) {
620
+ return state.incomplete(1, expectedLineEnding)
621
+ }
622
+ if (stop === internal.Invalid) return backtrack(state, "LineEnding", expectedLineEnding, offset)
623
+ if (stop === 0 && internal.next(enc, state, offset) === 13) {
624
+ const after = internal.next(enc, state, offset + internal.width)
625
+ if (after === internal.End && state.partial) return state.incomplete(1, expectedLineEnding)
626
+ if (after !== 10) return backtrack(state, "LineEnding", expectedLineEnding, offset)
627
+ }
628
+ state.pos = offset
629
+ return enc.text(state.input, start, offset)
630
+ }, "tillLineEnding", { _tag: "TillLineEnding", encoding: enc })
631
+
632
+ const asciiDigits = takeWhileText(isAsciiDigit, { min: 1 }, false, "digit1", ["digit"], "Digit", true)
633
+ const hexDigits = takeWhileText(isAsciiHexDigit, { min: 1 }, false, "hexDigit1", ["hexadecimal digit"], "HexDigit", true)
634
+ const sign = P.opt(oneOfImpl("+-", false))
635
+ const signed = taken(P.seq(sign, asciiDigits))
636
+
637
+ const exponent = P.opt(P.seq(oneOfImpl("eE", false), sign, asciiDigits))
638
+ const unsignedFloat = P.alt(
639
+ P.seq(caseless("inf"), P.opt(caseless("inity"))),
640
+ P.seq(literal("."), asciiDigits, exponent),
641
+ P.seq(asciiDigits, P.opt(P.seq(literal("."), P.opt(asciiDigits))), exponent)
642
+ )
643
+ const floatText = markFloatText(taken(P.alt(caseless("nan"), P.seq(sign, unsignedFloat))).pipe(P.expected("number")))
644
+
645
+ const matchControl = (control: string) => {
646
+ const ctrl = control.codePointAt(0)!
647
+ return (state: State<any>): boolean => {
648
+ if (internal.next(enc, state) !== ctrl) return false
649
+ state.pos += internal.width
650
+ return true
651
+ }
652
+ }
653
+
654
+ return {
655
+ encoding,
656
+ any,
657
+ literal,
658
+ caseless,
659
+ oneOf: (set) => oneOfImpl(set, false),
660
+ noneOf: (set) => oneOfImpl(set, true),
661
+ bom,
662
+ take,
663
+ takeWhile: (set, r) => {
664
+ const m = compile(set, "string")
665
+ return takeWhileText(m.code, r, false, "takeWhile", [m.description], "TakeWhile", false, set)
666
+ },
667
+ takeTill: (set, r) => {
668
+ const m = compile(set, "string")
669
+ return takeWhileText(m.code, r, true, "takeTill", [`anything but ${m.description}`], "TakeWhile", false, set)
670
+ },
671
+ takeUntil,
672
+ rest,
673
+ taken,
674
+ alpha0: cls(isAlphabetic, 0, "letter", "Alpha"),
675
+ alpha1: cls(isAlphabetic, 1, "letter", "Alpha"),
676
+ digit0: cls(isDecimalDigit, 0, "digit", "Digit"),
677
+ digit1: cls(isDecimalDigit, 1, "digit", "Digit"),
678
+ alphanumeric0: cls(isAlphanumeric, 0, "letter or digit", "AlphaNumeric"),
679
+ alphanumeric1: cls(isAlphanumeric, 1, "letter or digit", "AlphaNumeric"),
680
+ space0: cls(isSpace, 0, "space", "Space", false),
681
+ space1: cls(isSpace, 1, "space", "Space", false),
682
+ multispace0: cls(isWhitespace, 0, "whitespace", "MultiSpace", false),
683
+ multispace1: cls(isWhitespace, 1, "whitespace", "MultiSpace", false),
684
+ identifier,
685
+ newline,
686
+ crlf,
687
+ tab: literal("\t"),
688
+ lineEnding: P.alt(newline, crlf).pipe(P.expected("line ending")),
689
+ tillLineEnding,
690
+ decUint: asciiDigits.pipe(P.tryMap((s) => overflow(Number(s))), P.named("decUint")),
691
+ decInt: signed.pipe(P.tryMap((s) => overflow(Number(s))), P.named("decInt")),
692
+ decBigUint: asciiDigits.pipe(P.map((s) => BigInt(s))),
693
+ decBigInt: signed.pipe(P.map((s) => BigInt(s.startsWith("+") ? s.slice(1) : s))),
694
+ hexUint: hexDigits.pipe(P.tryMap((s) => overflow(parseInt(s, 16))), P.named("hexUint")),
695
+ float: floatText.pipe(P.map(toFloat), P.named("float")),
696
+ takeEscaped: (normal, control, escapable) =>
697
+ takeEscapedImpl(
698
+ normal,
699
+ matchControl(control),
700
+ escapable,
701
+ (state, start) => enc.text(state.input, start, state.pos),
702
+ control.codePointAt(0)!,
703
+ enc
704
+ ),
705
+ escaped: (normal, control, escape) =>
706
+ escapedImpl(normal, matchControl(control), escape, control.codePointAt(0)!, enc)
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Text parsers for JavaScript strings.
712
+ *
713
+ * @since 0.1.0
714
+ * @category instances
715
+ */
716
+ export const string: TextParsers<string> = make(Encodings.string)
717
+
718
+ /**
719
+ * Text parsers for UTF-8 encoded bytes.
720
+ *
721
+ * @example
722
+ * ```ts
723
+ * import { Parser, Text } from "@thomasfosterau/effect-parse"
724
+ *
725
+ * const bytes = new TextEncoder().encode("naïve café")
726
+ * Parser.parse(Parser.separatedPair(Text.utf8.alpha1, Text.utf8.space1, Text.utf8.alpha1), bytes)
727
+ * // Result.succeed(["naïve", "café"])
728
+ * ```
729
+ *
730
+ * @since 0.1.0
731
+ * @category instances
732
+ */
733
+ export const utf8: TextParsers<Uint8Array> = make(Encodings.utf8)
734
+
735
+ /**
736
+ * Text parsers for UTF-16 little endian bytes.
737
+ *
738
+ * @since 0.1.0
739
+ * @category instances
740
+ */
741
+ export const utf16le: TextParsers<Uint8Array> = make(Encodings.utf16le)
742
+
743
+ /**
744
+ * Text parsers for UTF-16 big endian bytes.
745
+ *
746
+ * @since 0.1.0
747
+ * @category instances
748
+ */
749
+ export const utf16be: TextParsers<Uint8Array> = make(Encodings.utf16be)
750
+
751
+ /**
752
+ * Text parsers for ISO-8859-1 (Latin-1) bytes.
753
+ *
754
+ * @since 0.1.0
755
+ * @category instances
756
+ */
757
+ export const latin1: TextParsers<Uint8Array> = make(Encodings.latin1)