+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 | +2x + +2x +113x + + + +113x + +1x + +111x + +1x + + + + +1x + + + +111x +1x + + +110x +110x + +1x + + +109x +108x + + + + +108x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +131x +21x + + + +110x +110x + + + + + + + + +3x +8x +2x + + +1x + + +3x +8x +1x + + +2x + + + +16x + + + + + + +10x + + +9x + + + + +4x + + +58x + + +2x +2x + + +2x + + + +2x + + +2x + + +1x + + + + + +235x +6x + + + +229x +216x + + + +13x + + + +14x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +10x + + + +10x + + + +10x + + + + +10x + + +3x + + +3x + + +2x + + +2x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +16x + + + +16x +6x + + +16x +4x +4x + + + + + + +16x + + +14x + + +2x + + + + + + +4x + + + +4x + + + +58x + + + +58x + + + + +2x + + + + +2x + + + +2x + + + + + + +52x +38x + + + +14x + + + +6x + + +8x + + + +8x + + + + + + + +58x +92x +92x +92x +92x + +6x + + + +86x +86x +1x + + + + + +85x +32x +32x + + +53x + +31x +75x +75x + +30x +3x + + +27x +27x + + +45x + + + + + +26x + + +25x + + + +92x + + + + + +92x +41x +41x + + +92x + + +92x +268x + + +12x +11x + +12x + +34x +34x + +30x +30x + +34x +34x + + + + +92x +92x +92x + + + + + + +161x + + + + + +161x +225x +7x + + +218x + +32x +32x +32x + +32x +32x +8x +8x + + + +32x +32x +32x +60x +19x +19x + + +41x +41x +41x +41x +41x +10x + + +31x +31x +10x +10x +3x + + + +28x +12x + + +28x + + +19x +6x + +13x + +14x +14x +14x + +4x +4x +1x +1x + + + +171x +71x + +100x +100x + + + +63x +63x +63x +63x + + + + +51x + + + + + +51x +12x +12x + + +39x +8x +8x + + + + + + + +39x +39x +39x +1x + + +39x + + | import * as Store from './store'
+import get from 'dlv'
+
+export default function matches(event, matcher: Store.Matcher): boolean {
+ Iif (!matcher) {
+ throw new Error('No matcher supplied!')
+ }
+
+ switch (matcher.type) {
+ case 'all':
+ return all()
+ case 'fql':
+ return fql(matcher.ir, event)
+ default:
+ throw new Error(`Matcher of type ${matcher.type} unsupported.`)
+ }
+}
+
+function all(): boolean {
+ return true
+}
+
+function fql(ir: Store.Matcher['ir'], event): boolean {
+ if (!ir) {
+ return false
+ }
+
+ try {
+ ir = JSON.parse(ir)
+ } catch (e) {
+ throw new Error(`Failed to JSON.parse FQL intermediate representation "${ir}": ${e}`)
+ }
+
+ const result = fqlEvaluate(ir, event)
+ Iif (typeof result !== 'boolean') {
+ // An error was returned, or a lowercase, typeof, or similar function was run alone. Nothing to evaluate.
+ return false
+ }
+
+ return result
+}
+
+type FQLEvaluator =
+ | ((event: any) => boolean)
+ | ((event: any) => string)
+ | ((event: any) => number)
+ | string
+ | number
+ | boolean
+ | Array<String | boolean | number>
+
+// Creates a function for evaluating a given FQL
+function generateEvaluator(ir: string): FQLEvaluator {
+ // If the given ir chunk is not an array, then we should check the single given path or value for literally `true`.
+ Iif (!Array.isArray) {
+ return (event: any) => getValue(ir, event) === true
+ }
+
+ const item = ir[0]
+
+ switch (item) {
+ /*** Unary cases ***/
+ // '!' => Invert the result
+ case '!':
+ const op = generateEvaluator(ir[1])
+ if (typeof op === 'function') {
+ return (event: any) => {
+ return !op(event)
+ }
+ } else {
+ return !op
+ }
+ /*** Binary cases ***/
+ // 'or' => Any condition being true returns true
+ case 'or':
+ const orOps: FQLEvaluator[] = []
+ for (let i = 1; i < ir.length; i++) {
+ orOps.push(generateEvaluator(ir[i]))
+ }
+ return (event: any) => {
+ for (const op of orOps) {
+ if (typeof op === 'function') {
+ Iif (op(event)) {
+ return true
+ }
+ } else {
+ Iif (op) return true
+ }
+ }
+ return false
+ }
+
+ // 'and' => Any condition being false returns false
+ case 'and':
+ const andOps: FQLEvaluator[] = []
+ for (let i = 1; i < ir.length; i++) {
+ andOps.push(generateEvaluator(ir[i]))
+ }
+ return (event: any) => {
+ for (const op of andOps) {
+ if (typeof op === 'function') {
+ Iif (!op(event)) {
+ return false
+ }
+ } else {
+ Iif (!op) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+
+ // Equivalence comparisons
+ case '=':
+ case '!=':
+ return compareItemsGenerator(ir[1], ir[2], item)
+ // Numerical comparisons
+ case '<=':
+ case '<':
+ case '>':
+ case '>=':
+ // Compare the two values with the given operator.
+ return compareNumbersGenerator(ir[1], ir[2], item)
+ // item in [list]' => Checks whether item is in list
+ case 'in':
+ return (event) => {
+ return checkInList(getValue(ir[1], event), getValue(ir[2], event), event)
+ }
+
+ /*** Functions ***/
+ // 'contains(str1, str2)' => The first string has a substring of the second string
+ case 'contains':
+ return (event) => {
+ return contains(getValue(ir[1], event), getValue(ir[2], event))
+ }
+ // 'match(str, match)' => The given string matches the provided glob matcher
+ case 'match':
+ return (event) => {
+ return match(getValue(ir[1], event), getValue(ir[2], event))
+ }
+ // 'lowercase(str)' => Returns a lowercased string, null if the item is not a string
+ case 'lowercase':
+ return (event) => {
+ const target = getValue(ir[1], event)
+ Iif (typeof target !== 'string') {
+ return null
+ }
+ return target.toLowerCase()
+ }
+ // 'typeof(val)' => Returns the FQL type of the value
+ case 'typeof':
+ // TODO: Do we need mapping to allow for universal comparisons? e.g. Object -> JSON, Array -> List, Floats?
+ return (event) => {
+ return typeof getValue(ir[1], event)
+ }
+ // 'length(val)' => Returns the length of an array or string, NaN if neither
+ case 'length':
+ return (event) => {
+ return length(getValue(ir[1], event))
+ }
+ // If nothing hit, we or the IR messed up somewhere.
+ default:
+ throw new Error(`FQL IR could not evaluate for token: ${item}`)
+ }
+}
+
+// FQL is 100% type strict in Go. Show no mercy to types which do not comply.
+function fqlEvaluate(ir, event) {
+ // If the given ir chunk is not an array, then we should check the single given path or value for literally `true`.
+ if (!Array.isArray(ir)) {
+ return getValue(ir, event) === true
+ }
+
+ // Otherwise, it is a sequence of ordered steps to follow to reach our solution!
+ const item = ir[0]
+ switch (item) {
+ /*** Unary cases ***/
+ // '!' => Invert the result
+ case '!':
+ return !fqlEvaluate(ir[1], event)
+
+ /*** Binary cases ***/
+ // 'or' => Any condition being true returns true
+ case 'or':
+ for (let i = 1; i < ir.length; i++) {
+ if (fqlEvaluate(ir[i], event)) {
+ return true
+ }
+ }
+ return false
+ // 'and' => Any condition being false returns false
+ case 'and':
+ for (let i = 1; i < ir.length; i++) {
+ if (!fqlEvaluate(ir[i], event)) {
+ return false
+ }
+ }
+ return true
+ // Equivalence comparisons
+ case '=':
+ case '!=':
+ return compareItems(getValue(ir[1], event), getValue(ir[2], event), item, event)
+ // Numerical comparisons
+ case '<=':
+ case '<':
+ case '>':
+ case '>=':
+ // Compare the two values with the given operator.
+ return compareNumbers(getValue(ir[1], event), getValue(ir[2], event), item, event)
+ // item in [list]' => Checks whether item is in list
+ case 'in':
+ return checkInList(getValue(ir[1], event), getValue(ir[2], event), event)
+
+ /*** Functions ***/
+ // 'contains(str1, str2)' => The first string has a substring of the second string
+ case 'contains':
+ return contains(getValue(ir[1], event), getValue(ir[2], event))
+ // 'match(str, match)' => The given string matches the provided glob matcher
+ case 'match':
+ return match(getValue(ir[1], event), getValue(ir[2], event))
+ // 'lowercase(str)' => Returns a lowercased string, null if the item is not a string
+ case 'lowercase':
+ const target = getValue(ir[1], event)
+ Iif (typeof target !== 'string') {
+ return null
+ }
+ return target.toLowerCase()
+ // 'typeof(val)' => Returns the FQL type of the value
+ case 'typeof':
+ // TODO: Do we need mapping to allow for universal comparisons? e.g. Object -> JSON, Array -> List, Floats?
+ return typeof getValue(ir[1], event)
+ // 'length(val)' => Returns the length of an array or string, NaN if neither
+ case 'length':
+ return length(getValue(ir[1], event))
+ // If nothing hit, we or the IR messed up somewhere.
+ default:
+ throw new Error(`FQL IR could not evaluate for token: ${item}`)
+ }
+}
+
+function getValue(item, event) {
+ // If item is an array, leave it as-is.
+ if (Array.isArray(item)) {
+ return item
+ }
+
+ // If item is an object, it has the form of `{"value": VAL}`
+ if (typeof item === 'object') {
+ return item.value
+ }
+
+ // Otherwise, it's an event path, e.g. "properties.email"
+ return get(event, item)
+}
+
+function checkInList(item, list, event): boolean {
+ return list.find((it) => getValue(it, event) === item) !== undefined
+}
+
+function compareNumbersGenerator(first, second, operator): (event) => boolean {
+ // Check if it's more IR (such as a length() function)
+ let firstEval: FQLEvaluator
+ let secondEval: FQLEvaluator
+
+ Iif (isIR(first)) {
+ firstEval = generateEvaluator(first)
+ }
+
+ Iif (isIR(second)) {
+ secondEval = generateEvaluator(second)
+ }
+
+ // Reminder: NaN is not comparable to any other number (including NaN) and will always return false as desired.
+ let op: (a: number, b: number) => boolean
+ switch (operator) {
+ // '<=' => The first number is less than or equal to the second.
+ case '<=':
+ op = (a, b) => a <= b
+ break
+ // '>=' => The first number is greater than or equal to the second
+ case '>=':
+ op = (a, b) => a >= b
+ break
+ // '<' The first number is less than the second.
+ case '<':
+ op = (a, b) => a < b
+ break
+ // '>' The first number is greater than the second.
+ case '>':
+ op = (a, b) => a > b
+ break
+ default:
+ throw new Error(`Invalid operator in compareNumbers: ${operator}`)
+ }
+
+ return (event) => {
+ let a, b
+
+ if (typeof firstEval === 'function') {
+ a = firstEval(event)
+ } else {
+ a = firstEval
+ }
+
+ if (typeof secondEval === 'function') {
+ b = secondEval(event)
+ } else {
+ b = secondEval
+ }
+
+ Iif (typeof a !== 'number' || typeof b !== 'number') {
+ return false
+ }
+
+ return op(a, b)
+ }
+}
+
+function compareNumbers(first, second, operator, event): boolean {
+ // Check if it's more IR (such as a length() function)
+ Iif (isIR(first)) {
+ first = fqlEvaluate(first, event)
+ }
+
+ Iif (isIR(second)) {
+ second = fqlEvaluate(second, event)
+ }
+
+ Iif (typeof first !== 'number' || typeof second !== 'number') {
+ return false
+ }
+
+ // Reminder: NaN is not comparable to any other number (including NaN) and will always return false as desired.
+ switch (operator) {
+ // '<=' => The first number is less than or equal to the second.
+ case '<=':
+ return first <= second
+ // '>=' => The first number is greater than or equal to the second
+ case '>=':
+ return first >= second
+ // '<' The first number is less than the second.
+ case '<':
+ return first < second
+ // '>' The first number is greater than the second.
+ case '>':
+ return first > second
+ default:
+ throw new Error(`Invalid operator in compareNumbers: ${operator}`)
+ }
+}
+
+function compareItemsGenerator(first, second, operator): FQLEvaluator {
+ let firstEval: FQLEvaluator
+ let secondEval: FQLEvaluator
+
+ // Check if it's more IR (such as a lowercase() function)
+ Iif (isIR(first)) {
+ firstEval = generateEvaluator(first)
+ }
+
+ Iif (isIR(second)) {
+ secondEval = generateEvaluator(second)
+ }
+
+ Iif (typeof first === 'object' && typeof second === 'object') {
+ firstEval = JSON.stringify(first)
+ secondEval = JSON.stringify(second)
+ }
+
+ // Objects with the exact same contents AND order ARE considered identical. (Don't compare by reference)
+ // Even in Go, this MUST be the same byte order.
+ // e.g. {a: 1, b:2} === {a: 1, b:2} BUT {a:1, b:2} !== {b:2, a:1}
+ // Maybe later we'll use a stable stringifier, but we're matching server-side behavior for now.
+ let compareOp: (a: unknown, b: unknown) => boolean
+ switch (operator) {
+ // '=' => The two following items are exactly identical
+ case '=':
+ compareOp = (a, b) => a === b
+ break
+ // '!=' => The two following items are NOT exactly identical.
+ case '!=':
+ compareOp = (a, b) => a !== b
+ break
+ default:
+ throw new Error(`Invalid operator in compareItems: ${operator}`)
+ }
+
+ return (event) => {
+ let a, b
+
+ if (typeof firstEval === 'function') {
+ a = firstEval(event)
+ } else {
+ a = firstEval
+ }
+
+ if (typeof secondEval === 'function') {
+ b = secondEval(event)
+ } else {
+ b = secondEval
+ }
+
+ return compareOp(a, b)
+ }
+}
+
+function compareItems(first, second, operator, event): boolean {
+ // Check if it's more IR (such as a lowercase() function)
+ Iif (isIR(first)) {
+ first = fqlEvaluate(first, event)
+ }
+
+ if (isIR(second)) {
+ second = fqlEvaluate(second, event)
+ }
+
+ if (typeof first === 'object' && typeof second === 'object') {
+ first = JSON.stringify(first)
+ second = JSON.stringify(second)
+ }
+
+ // Objects with the exact same contents AND order ARE considered identical. (Don't compare by reference)
+ // Even in Go, this MUST be the same byte order.
+ // e.g. {a: 1, b:2} === {a: 1, b:2} BUT {a:1, b:2} !== {b:2, a:1}
+ // Maybe later we'll use a stable stringifier, but we're matching server-side behavior for now.
+ switch (operator) {
+ // '=' => The two following items are exactly identical
+ case '=':
+ return first === second
+ // '!=' => The two following items are NOT exactly identical.
+ case '!=':
+ return first !== second
+ default:
+ throw new Error(`Invalid operator in compareItems: ${operator}`)
+ }
+}
+
+function contains(first, second): boolean {
+ Iif (typeof first !== 'string' || typeof second !== 'string') {
+ return false
+ }
+
+ return first.indexOf(second) !== -1
+}
+
+function match(str, glob): boolean {
+ Iif (typeof str !== 'string' || typeof glob !== 'string') {
+ return false
+ }
+
+ return globMatches(glob, str)
+}
+
+function length(item) {
+ // Match server-side behavior.
+ Iif (item === null) {
+ return 0
+ }
+
+ // Type-check to avoid returning .length of an object
+ Iif (!Array.isArray(item) && typeof item !== 'string') {
+ return NaN
+ }
+
+ return item.length
+}
+
+// This is a heuristic technically speaking, but should be close enough. The odds of someone trying to test
+// a func with identical IR notation is pretty low.
+function isIR(value): boolean {
+ // TODO: This can be better checked by checking if this is a {"value": THIS}
+ if (!Array.isArray(value)) {
+ return false
+ }
+
+ // Function checks
+ if (
+ (value[0] === 'lowercase' || value[0] === 'length' || value[0] === 'typeof') &&
+ value.length === 2
+ ) {
+ return true
+ }
+
+ Iif ((value[0] === 'contains' || value[0] === 'match') && value.length === 3) {
+ return true
+ }
+
+ return false
+}
+
+// Any reputable glob matcher is designed to work on filesystems and doesn't allow the override of the separator
+// character "/". This is problematic since our server-side representation e.g. evaluates "match('ab/c', 'a*)"
+// as TRUE, whereas any glob matcher for JS available does false. So we're rewriting it here.
+// See: https://github.com/segmentio/glob/blob/master/glob.go
+function globMatches(pattern, str): boolean {
+ Pattern: while (pattern.length > 0) {
+ let star
+ let chunk
+ ;({ star, chunk, pattern } = scanChunk(pattern))
+ if (star && chunk === '') {
+ // Trailing * matches rest of string
+ return true
+ }
+
+ // Look for match at current position
+ let { t, ok, err } = matchChunk(chunk, str)
+ if (err) {
+ return false
+ }
+
+ // If we're the last chunk, make sure we've exhausted the str
+ // otherwise we'll give a false result even if we could still match
+ // using the star
+ if (ok && (t.length === 0 || pattern.length > 0)) {
+ str = t
+ continue
+ }
+
+ if (star) {
+ // Look for match, skipping i+1 bytes.
+ for (let i = 0; i < str.length; i++) {
+ ;({ t, ok, err } = matchChunk(chunk, str.slice(i + 1)))
+ if (ok) {
+ // If we're the last chunk, make sure we exhausted the str.
+ if (pattern.length === 0 && t.length > 0) {
+ continue
+ }
+
+ str = t
+ continue Pattern
+ }
+
+ Iif (err) {
+ return false
+ }
+ }
+ }
+
+ return false
+ }
+
+ return str.length === 0
+}
+
+function scanChunk(pattern): any {
+ const result = {
+ star: false,
+ chunk: '',
+ pattern: '',
+ }
+
+ while (pattern.length > 0 && pattern[0] === '*') {
+ pattern = pattern.slice(1)
+ result.star = true
+ }
+
+ let inRange = false
+ let i
+
+ Scan: for (i = 0; i < pattern.length; i++) {
+ switch (pattern[i]) {
+ case '\\':
+ // Error check handled in matchChunk: bad pattern.
+ if (i + 1 < pattern.length) {
+ i++
+ }
+ break
+ case '[':
+ inRange = true
+ break
+ case ']':
+ inRange = false
+ break
+ case '*':
+ if (!inRange) {
+ break Scan
+ }
+ }
+ }
+
+ result.chunk = pattern.slice(0, i)
+ result.pattern = pattern.slice(i)
+ return result
+}
+
+// matchChunk checks whether chunk matches the beginning of s.
+// If so, it returns the remainder of s (after the match).
+// Chunk is all single-character operators: literals, char classes, and ?.
+function matchChunk(chunk, str): any {
+ const result = {
+ t: '',
+ ok: false,
+ err: false,
+ }
+
+ while (chunk.length > 0) {
+ if (str.length === 0) {
+ return result
+ }
+
+ switch (chunk[0]) {
+ case '[':
+ const char = str[0]
+ str = str.slice(1)
+ chunk = chunk.slice(1)
+
+ let notNegated = true
+ if (chunk.length > 0 && chunk[0] === '^') {
+ notNegated = false
+ chunk = chunk.slice(1)
+ }
+
+ // Parse all ranges
+ let foundMatch = false
+ let nRange = 0
+ while (true) {
+ if (chunk.length > 0 && chunk[0] === ']' && nRange > 0) {
+ chunk = chunk.slice(1)
+ break
+ }
+
+ let lo = ''
+ let hi = ''
+ let err
+ ;({ char: lo, newChunk: chunk, err } = getEsc(chunk))
+ if (err) {
+ return result
+ }
+
+ hi = lo
+ if (chunk[0] === '-') {
+ ;({ char: hi, newChunk: chunk, err } = getEsc(chunk.slice(1)))
+ if (err) {
+ return result
+ }
+ }
+
+ if (lo <= char && char <= hi) {
+ foundMatch = true
+ }
+
+ nRange++
+ }
+
+ if (foundMatch !== notNegated) {
+ return result
+ }
+ break
+ case '?':
+ str = str.slice(1)
+ chunk = chunk.slice(1)
+ break
+ case '\\':
+ chunk = chunk.slice(1)
+ if (chunk.length === 0) {
+ result.err = true
+ return result
+ }
+ // Fallthrough, missing break intentional.
+ default:
+ if (chunk[0] !== str[0]) {
+ return result
+ }
+ str = str.slice(1)
+ chunk = chunk.slice(1)
+ }
+ }
+
+ result.t = str
+ result.ok = true
+ result.err = false
+ return result
+}
+
+// getEsc gets a possibly-escaped character from chunk, for a character class.
+function getEsc(chunk): any {
+ const result = {
+ char: '',
+ newChunk: '',
+ err: false,
+ }
+
+ if (chunk.length === 0 || chunk[0] === '-' || chunk[0] === ']') {
+ result.err = true
+ return result
+ }
+
+ if (chunk[0] === '\\') {
+ chunk = chunk.slice(1)
+ Iif (chunk.length === 0) {
+ result.err = true
+ return result
+ }
+ }
+
+ // Unlike Go, JS strings operate on characters instead of bytes.
+ // This is why we aren't copying over the GetRuneFromString stuff.
+ result.char = chunk[0]
+ result.newChunk = chunk.slice(1)
+ if (result.newChunk.length === 0) {
+ result.err = true
+ }
+
+ return result
+}
+ |