shout/src/match.ts

104 lines
2.7 KiB
TypeScript

import { escapeRegex } from "./utils.ts"
export function matchLine(pattern: string, actual: string): boolean {
if (!pattern.includes("...")) return pattern === actual
// Convert inline ... to regex
const parts = pattern.split("...")
const escaped = parts.map(p => escapeRegex(p))
const regex = new RegExp("^" + escaped.join(".*") + "$")
return regex.test(actual)
}
export function matchOutput(
expected: string[],
actual: string[],
): boolean {
return doMatch(expected, 0, actual, 0)
}
function doMatch(
expected: string[],
ei: number,
actual: string[],
ai: number,
): boolean {
// Both exhausted — match
if (ei === expected.length && ai === actual.length) return true
// Expected exhausted but actual remains — no match
if (ei === expected.length) return false
const exp = expected[ei]!
// Multi-line wildcard
if (exp === "...") {
// Try matching zero or more actual lines
for (let skip = ai; skip <= actual.length; skip++) {
if (doMatch(expected, ei + 1, actual, skip)) return true
}
return false
}
// Actual exhausted but expected remains — no match
if (ai === actual.length) return false
// Line-level match (with possible inline wildcards)
if (matchLine(exp, actual[ai]!)) {
return doMatch(expected, ei + 1, actual, ai + 1)
}
return false
}
export type DiffLine = {
kind: "equal" | "expected" | "actual" | "context"
text: string
}
export function diff(expected: string[], actual: string[]): DiffLine[] {
const result: DiffLine[] = []
let ei = 0
let ai = 0
while (ei < expected.length || ai < actual.length) {
if (ei < expected.length && expected[ei] === "...") {
// Find where the wildcard ends by looking at next expected line
const nextExp = ei + 1 < expected.length ? expected[ei + 1] : null
if (nextExp === null) {
// ... at end matches everything remaining
result.push({ kind: "context", text: "..." })
break
}
// Skip actual lines until we find the next expected match
result.push({ kind: "context", text: "..." })
ei++
while (ai < actual.length && !matchLine(nextExp!, actual[ai]!)) {
ai++
}
continue
}
if (ei < expected.length && ai < actual.length) {
if (matchLine(expected[ei]!, actual[ai]!)) {
result.push({ kind: "equal", text: actual[ai]! })
ei++
ai++
} else {
result.push({ kind: "expected", text: expected[ei]! })
result.push({ kind: "actual", text: actual[ai]! })
ei++
ai++
}
} else if (ei < expected.length) {
result.push({ kind: "expected", text: expected[ei]! })
ei++
} else {
result.push({ kind: "actual", text: actual[ai]! })
ai++
}
}
return result
}