Spaces:
Sleeping
Sleeping
Download src/lib/pattern-engine.ts from iq7se2/tg-tracker: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/spaces/iq7se2/tg-tracker/resolve/main/src/lib/pattern-engine.ts
- Command line
-
hf download hf://spaces/iq7se2/tg-tracker/src/lib/pattern-engine.ts
-
curl -L -o pattern-engine.ts https://huggingface.co/spaces/iq7se2/tg-tracker/resolve/main/src/lib/pattern-engine.ts
10.8 kB
| /** | |
| * Pattern Matching Engine | |
| * | |
| * DSL syntax: | |
| * - lowercase letters a-z: variable placeholder. Same letter = same character. | |
| * Different letters = independent variables (may coincidentally be equal). | |
| * Matches [a-z0-9]. | |
| * - digits 0-9: variable placeholder (same rules as letters). Matches [a-z0-9]. | |
| * - `#`: identical-run marker. A maximal run of `#` matches a run of identical | |
| * characters. The first `#` starts a new group, subsequent `#`s backreference it. | |
| * Matches [a-z0-9] (valid Telegram username chars excluding literal `_`). | |
| * - `_`: literal underscore. | |
| * - `\X`: literal character X (escape for when you need a literal a-z / 0-9 / #). | |
| * - `[literal]`: a literal block — everything between `[` and `]` is treated as | |
| * literal text (no variable interpretation). Useful for fixed words like | |
| * `[vip]ab` which matches "vip" + two variables (vipdd, vipx1, vipab). | |
| * - any other character: treated as a literal (regex-escaped). | |
| * | |
| * Examples (all match the user's listed usernames): | |
| * - "a_b_0" -> ^([a-z0-9])_([a-z0-9])_([a-z0-9])$ matches e_y_3, e_e_h, a_k_v | |
| * - "ab###" -> ^([a-z0-9])([a-z0-9])([a-z0-9])\3\3$ matches av000 | |
| * - "###ab" -> ^([a-z0-9])\1\1([a-z0-9])([a-z0-9])$ matches tttxy | |
| * - "a###a" -> ^([a-z0-9])([a-z0-9])\2\2\1$ matches u000u | |
| * - "a###b" -> ^([a-z0-9])([a-z0-9])\2\2([a-z0-9])$ matches u000q, kjjjf | |
| * - "aa###" -> ^([a-z0-9])\1([a-z0-9])\3\3$ matches aaccc | |
| * - "###bb" -> ^([a-z0-9])\1\1([a-z0-9])\4$ matches gggbb | |
| * - "[vip]ab" -> ^vip([a-z0-9])([a-z0-9])$ matches vipdd, vipx1 | |
| */ | |
| export interface CompiledPattern { | |
| regex: string; | |
| flags: string; | |
| variableCount: number; | |
| length: number; | |
| } | |
| export interface PatternCompileError { | |
| error: string; | |
| } | |
| const USERNAME_CHAR = "[a-z0-9]"; | |
| function escapeRegex(s: string): string { | |
| return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |
| } | |
| /** | |
| * Compile a DSL expression into an anchored regex source string. | |
| */ | |
| export function compilePattern(expr: string): CompiledPattern { | |
| if (!expr || expr.length === 0) { | |
| throw new Error("Pattern expression cannot be empty"); | |
| } | |
| if (expr.length < 3) { | |
| throw new Error("Pattern is too short (Telegram usernames are 5-32 chars, patterns should be at least 3)"); | |
| } | |
| if (expr.length > 32) { | |
| throw new Error("Pattern is too long (Telegram usernames are max 32 chars)"); | |
| } | |
| let regex = ""; | |
| const varGroups = new Map<string, number>(); | |
| let groupCount = 0; | |
| let i = 0; | |
| while (i < expr.length) { | |
| const ch = expr[i]; | |
| // Escape sequence -> literal | |
| if (ch === "\\" && i + 1 < expr.length) { | |
| regex += escapeRegex(expr[i + 1]); | |
| i += 2; | |
| continue; | |
| } | |
| // Literal block [....] -> everything inside is literal text | |
| if (ch === "[") { | |
| const end = expr.indexOf("]", i + 1); | |
| if (end === -1) { | |
| throw new Error("Unclosed [ literal block — missing ]"); | |
| } | |
| const literalContent = expr.slice(i + 1, end); | |
| if (literalContent.length === 0) { | |
| throw new Error("Empty [ ] literal block"); | |
| } | |
| regex += escapeRegex(literalContent); | |
| i = end + 1; | |
| continue; | |
| } | |
| // Identical run of # | |
| if (ch === "#") { | |
| let runLen = 0; | |
| while (i < expr.length && expr[i] === "#") { | |
| runLen++; | |
| i++; | |
| } | |
| groupCount++; | |
| const groupNum = groupCount; | |
| regex += `(${USERNAME_CHAR})`; | |
| for (let r = 1; r < runLen; r++) { | |
| regex += `\\${groupNum}`; | |
| } | |
| continue; | |
| } | |
| // Variable (letter or digit) | |
| if (/[a-z0-9]/.test(ch)) { | |
| if (varGroups.has(ch)) { | |
| regex += `\\${varGroups.get(ch)}`; | |
| } else { | |
| groupCount++; | |
| varGroups.set(ch, groupCount); | |
| regex += `(${USERNAME_CHAR})`; | |
| } | |
| i++; | |
| continue; | |
| } | |
| // Literal | |
| regex += escapeRegex(ch); | |
| i++; | |
| } | |
| return { | |
| regex: `^${regex}$`, | |
| flags: "i", | |
| variableCount: groupCount, | |
| length: expr.length, | |
| }; | |
| } | |
| /** | |
| * Build a RegExp from a compiled pattern. | |
| */ | |
| export function buildRegExp(compiled: CompiledPattern): RegExp { | |
| return new RegExp(compiled.regex, compiled.flags); | |
| } | |
| /** | |
| * Test whether a username (without @) matches a compiled pattern. | |
| */ | |
| export function matchesPattern(username: string, compiled: CompiledPattern): boolean { | |
| const re = buildRegExp(compiled); | |
| return re.test(username); | |
| } | |
| /** | |
| * Validate a Telegram username (basic rules). | |
| * - 5-32 chars | |
| * - a-z, 0-9, _ only | |
| * - must start with a letter | |
| * - no leading/trailing underscore | |
| * - no consecutive underscores | |
| */ | |
| export function isValidTelegramUsername(username: string): boolean { | |
| if (!username) return false; | |
| if (username.length < 5 || username.length > 32) return false; | |
| if (!/^[a-z]/.test(username)) return false; | |
| if (!/^[a-z0-9_]+$/.test(username)) return false; | |
| if (username.startsWith("_") || username.endsWith("_")) return false; | |
| if (username.includes("__")) return false; | |
| return true; | |
| } | |
| /** | |
| * Generate example usernames that match a pattern (for preview in the dashboard). | |
| * Returns up to `count` examples. | |
| */ | |
| export function generateExamples(expr: string, count = 5): string[] { | |
| let compiled: CompiledPattern; | |
| try { | |
| compiled = compilePattern(expr); | |
| } catch { | |
| return []; | |
| } | |
| const re = buildRegExp(compiled); | |
| const examples: string[] = []; | |
| const seen = new Set<string>(); | |
| // Deterministic generation: try a set of known-matching seeds + random attempts. | |
| // We brute-force over the pattern structure by building candidates from the DSL. | |
| const candidates = generateCandidates(expr); | |
| for (const c of candidates) { | |
| if (seen.has(c)) continue; | |
| seen.add(c); | |
| if (re.test(c) && isValidTelegramUsername(c)) { | |
| examples.push(c); | |
| if (examples.length >= count) break; | |
| } | |
| } | |
| // Random fill if not enough. | |
| let attempts = 0; | |
| while (examples.length < count && attempts < 2000) { | |
| attempts++; | |
| const candidate = randomUsernameForPattern(expr); | |
| if (candidate && !seen.has(candidate) && re.test(candidate) && isValidTelegramUsername(candidate)) { | |
| seen.add(candidate); | |
| examples.push(candidate); | |
| } | |
| } | |
| return examples; | |
| } | |
| /** | |
| * Build a small set of deterministic candidate usernames from the DSL expression | |
| * by substituting concrete values for each variable / run. | |
| */ | |
| function generateCandidates(expr: string): string[] { | |
| // Identify variable slots and run slots. | |
| const varNames = new Set<string>(); | |
| const runs: { start: number; len: number; varName: string }[] = []; | |
| let i = 0; | |
| const structure: { type: "var" | "run" | "literal"; value: string; runLen?: number; varName?: string }[] = []; | |
| while (i < expr.length) { | |
| const ch = expr[i]; | |
| if (ch === "\\" && i + 1 < expr.length) { | |
| structure.push({ type: "literal", value: expr[i + 1] }); | |
| i += 2; | |
| continue; | |
| } | |
| if (ch === "[") { | |
| const end = expr.indexOf("]", i + 1); | |
| if (end === -1) break; | |
| structure.push({ type: "literal", value: expr.slice(i + 1, end) }); | |
| i = end + 1; | |
| continue; | |
| } | |
| if (ch === "#") { | |
| let runLen = 0; | |
| let j = i; | |
| while (j < expr.length && expr[j] === "#") { | |
| runLen++; | |
| j++; | |
| } | |
| const varName = `__run${runs.length}`; | |
| runs.push({ start: i, len: runLen, varName }); | |
| varNames.add(varName); | |
| structure.push({ type: "run", value: "", runLen, varName }); | |
| i = j; | |
| continue; | |
| } | |
| if (/[a-z0-9]/.test(ch)) { | |
| varNames.add(ch); | |
| structure.push({ type: "var", value: "", varName: ch }); | |
| i++; | |
| continue; | |
| } | |
| structure.push({ type: "literal", value: ch }); | |
| i++; | |
| } | |
| const vars = Array.from(varNames); | |
| // Letter pool for variables (avoid _ for vars since _ is literal). | |
| const letterPool = "abcdefghijklmnopqrstuvwxyz".split(""); | |
| // For runs we allow letters and digits. | |
| const runPool = "abcdefghijklmnopqrstuvwxyz0123456789".split(""); | |
| const results: string[] = []; | |
| // Generate a few combinations. | |
| const combos: Record<string, string>[] = []; | |
| const maxCombos = 8; | |
| const pick = <T>(arr: T[], n: number): T => arr[n % arr.length]; | |
| for (let n = 0; n < maxCombos; n++) { | |
| const combo: Record<string, string> = {}; | |
| vars.forEach((v, idx) => { | |
| const isRun = v.startsWith("__run"); | |
| const pool = isRun ? runPool : letterPool; | |
| combo[v] = pick(pool, n + idx * 3); | |
| }); | |
| combos.push(combo); | |
| } | |
| for (const combo of combos) { | |
| let s = ""; | |
| for (const part of structure) { | |
| if (part.type === "literal") { | |
| s += part.value; | |
| } else if (part.type === "var") { | |
| s += combo[part.varName!]; | |
| } else if (part.type === "run") { | |
| s += combo[part.varName!].repeat(part.runLen!); | |
| } | |
| } | |
| results.push(s); | |
| } | |
| return results; | |
| } | |
| function randomUsernameForPattern(expr: string): string | null { | |
| const structure: { type: "var" | "run" | "literal"; value: string; runLen?: number; varName?: string }[] = []; | |
| const varNames = new Set<string>(); | |
| let i = 0; | |
| let runIdx = 0; | |
| while (i < expr.length) { | |
| const ch = expr[i]; | |
| if (ch === "\\" && i + 1 < expr.length) { | |
| structure.push({ type: "literal", value: expr[i + 1] }); | |
| i += 2; | |
| continue; | |
| } | |
| if (ch === "[") { | |
| const end = expr.indexOf("]", i + 1); | |
| if (end === -1) break; | |
| structure.push({ type: "literal", value: expr.slice(i + 1, end) }); | |
| i = end + 1; | |
| continue; | |
| } | |
| if (ch === "#") { | |
| let runLen = 0; | |
| let j = i; | |
| while (j < expr.length && expr[j] === "#") { | |
| runLen++; | |
| j++; | |
| } | |
| const varName = `__run${runIdx++}`; | |
| varNames.add(varName); | |
| structure.push({ type: "run", value: "", runLen, varName }); | |
| i = j; | |
| continue; | |
| } | |
| if (/[a-z0-9]/.test(ch)) { | |
| varNames.add(ch); | |
| structure.push({ type: "var", value: "", varName: ch }); | |
| i++; | |
| continue; | |
| } | |
| structure.push({ type: "literal", value: ch }); | |
| i++; | |
| } | |
| const letters = "abcdefghijklmnopqrstuvwxyz"; | |
| const alnum = "abcdefghijklmnopqrstuvwxyz0123456789"; | |
| const rand = <T>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)]; | |
| const combo: Record<string, string> = {}; | |
| for (const v of varNames) { | |
| const pool = v.startsWith("__run") ? alnum : letters; | |
| combo[v] = rand(pool.split("")); | |
| } | |
| let s = ""; | |
| for (const part of structure) { | |
| if (part.type === "literal") s += part.value; | |
| else if (part.type === "var") s += combo[part.varName!]; | |
| else if (part.type === "run") s += combo[part.varName!].repeat(part.runLen!); | |
| } | |
| return s; | |
| } | |