Initial Commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user