105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import { QWERTY_GRAPH } from "../../graphs/qwerty/qwerty.js";
|
|
import { validateGraph } from "../../graphs/validate-graph.js";
|
|
import type {
|
|
KeyboardGraph,
|
|
KeyboardGraphMap,
|
|
KeyboardMatch,
|
|
} from "../types.js";
|
|
|
|
export interface KeyboardMatchOptions {
|
|
graphs?: KeyboardGraphMap;
|
|
}
|
|
|
|
/**
|
|
* `keyboardMatch` — find every substring that traces a walk on a keyboard
|
|
* adjacency graph per Wheeler 2016 section 3.5. Each match carries its graph
|
|
* name, number of direction changes (`turns`), and number of shift-required
|
|
* presses (`shiftedCount`).
|
|
*/
|
|
export function keyboardMatch(
|
|
password: string,
|
|
opts: KeyboardMatchOptions = {}
|
|
): KeyboardMatch[] {
|
|
const graphs: KeyboardGraphMap = opts.graphs ?? { qwerty: QWERTY_GRAPH };
|
|
for (const [name, graph] of Object.entries(graphs)) {
|
|
validateGraph(name, graph);
|
|
}
|
|
const out: KeyboardMatch[] = [];
|
|
for (const [graphName, graph] of Object.entries(graphs)) {
|
|
out.push(...walkGraph(password, graphName, graph));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
interface SlotHit {
|
|
slot: number;
|
|
shifted: boolean;
|
|
}
|
|
|
|
function findSlot(
|
|
graph: KeyboardGraph,
|
|
prev: string,
|
|
next: string
|
|
): SlotHit | undefined {
|
|
const adjacencies = graph[prev];
|
|
if (!adjacencies) return undefined;
|
|
for (let s = 0; s < adjacencies.length; s++) {
|
|
const slot = adjacencies[s];
|
|
if (slot === null || slot === undefined) continue;
|
|
const unshifted = slot[0];
|
|
const shifted = slot[1];
|
|
if (next === unshifted) return { slot: s, shifted: false };
|
|
if (next === shifted) return { slot: s, shifted: true };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function isShifted(graph: KeyboardGraph, ch: string): boolean {
|
|
if (graph[ch]) return false;
|
|
for (const adjs of Object.values(graph)) {
|
|
for (const slot of adjs) {
|
|
if (slot && slot[1] === ch) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function walkGraph(
|
|
password: string,
|
|
graphName: string,
|
|
graph: KeyboardGraph
|
|
): KeyboardMatch[] {
|
|
const out: KeyboardMatch[] = [];
|
|
if (password.length < 2) return out;
|
|
let i = 0;
|
|
while (i < password.length - 1) {
|
|
let j = i;
|
|
let lastSlot = -1;
|
|
let turns = 0;
|
|
let shiftedCount = isShifted(graph, password[i] as string) ? 1 : 0;
|
|
while (j < password.length - 1) {
|
|
const cur = (password[j] as string).toLowerCase();
|
|
const next = password[j + 1] as string;
|
|
const hit = findSlot(graph, cur, next);
|
|
if (!hit) break;
|
|
if (lastSlot !== -1 && hit.slot !== lastSlot) turns++;
|
|
lastSlot = hit.slot;
|
|
if (hit.shifted) shiftedCount++;
|
|
j++;
|
|
}
|
|
if (j - i + 1 >= 2) {
|
|
out.push({
|
|
pattern: "spatial",
|
|
i,
|
|
j,
|
|
token: password.slice(i, j + 1),
|
|
graph: graphName,
|
|
turns,
|
|
shiftedCount,
|
|
});
|
|
}
|
|
i = j + 1;
|
|
}
|
|
return out;
|
|
}
|