Files
SnowLuma/.pnpm-store/v11/files/0b/67a594d80351bb069d223e19aea08e4db0e23f2b5e7a80a37953885109d9c6ec11f8a6311dbe32e79d18d3162aeaa824c0ed9754b6cc8e71a71f1793ae36f0
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

161 lines
6.3 KiB
Plaintext

import MatchL33t from '../dictionary/variants/matching/l33t.mjs';
import MatchReverse from '../dictionary/variants/matching/reverse.mjs';
import MatchDictionary from '../dictionary/matching.mjs';
import { MatcherBaseClass } from '../../types.mjs';
/*
*-------------------------------------------------------------------------------
* word sequences (oneTwoThree, fourFiveSix) ------------------------------
*-------------------------------------------------------------------------------
*/
class MatchWordSequence extends MatcherBaseClass {
constructor(options) {
super(options);
this.dictionary = new MatchDictionary(this.options, true);
this.dictionaryL33t = new MatchL33t(this.options, true);
this.dictionaryReverse = new MatchReverse(this.options, true);
}
match(matchOptions) {
const {
password
} = matchOptions;
// Get all dictionary matches first
const dictionaryMatches = this.dictionary.match(matchOptions);
const dictionaryL33tMatches = this.dictionaryL33t.match(matchOptions);
const dictionaryReverseMatches = this.dictionaryReverse.match(matchOptions);
const filteredDictionaryMatches = this.filterDictionaryMatches([...dictionaryMatches, ...dictionaryL33tMatches, ...dictionaryReverseMatches]);
// Convert dictionary matches to our internal format
const wordMatches = this.convertToWordMatches(filteredDictionaryMatches);
// Find sequences of consecutive words
return this.findWordSequences(wordMatches, password);
}
filterDictionaryMatches(matches) {
// sort by start index, then by end index
return [...matches].sort((a, b) => {
if (a.i !== b.i) return a.i - b.i;
if (a.j !== b.j) return a.j - b.j;
// prefer forward over reversed
if (a.reversed !== b.reversed) return a.reversed ? 1 : -1;
// prefer non-l33t
if (a.l33t !== b.l33t) return a.l33t ? 1 : -1;
// prefer better rank
return a.rank - b.rank;
})
// Keep only non-overlapping matches, favoring earlier ones
.reduce((acc, match) => {
const last = acc[acc.length - 1];
if (!last || match.i > last.j) {
acc.push(match);
}
return acc;
}, []);
}
convertToWordMatches(dictionaryMatches) {
return dictionaryMatches.map(match => ({
word: match.matchedWord,
i: match.i,
j: match.j,
rank: match.rank,
dictionaryName: match.dictionaryName
}));
}
findWordSequences(wordMatches, password) {
const sequences = [];
if (wordMatches.length === 0) {
return sequences;
}
// Sort matches by start position
const sortedMatches = [...wordMatches].sort((a, b) => a.i - b.i);
// Find all possible sequences
for (let startIdx = 0; startIdx < sortedMatches.length; startIdx += 1) {
const sequencesFromStart = this.findSequencesFromStart(sortedMatches, startIdx, password);
sequences.push(...sequencesFromStart);
}
return sequences;
}
// eslint-disable-next-line max-statements
findSequencesFromStart(sortedMatches, startIdx, password) {
const sequences = [];
const startMatch = sortedMatches[startIdx];
// Start with single word sequence
let currentSequence = [startMatch];
let currentEnd = startMatch.j;
// Try to extend the sequence
for (let nextIdx = startIdx + 1; nextIdx < sortedMatches.length; nextIdx += 1) {
const nextMatch = sortedMatches[nextIdx];
// Only extend if the next word can form a valid sequence
if (this.isValidWordSequence(currentSequence, nextMatch, password)) {
currentSequence.push(nextMatch);
currentEnd = nextMatch.j;
} else if (nextMatch.i > currentEnd) {
// Gap found, save current sequence and start new one
if (currentSequence.length > 1) {
sequences.push(this.createWordSequenceMatch(currentSequence, password));
}
currentSequence = [nextMatch];
currentEnd = nextMatch.j;
}
// If nextMatch.i <= currentEnd, it overlaps, so we skip it
}
// Don't forget the last sequence
if (currentSequence.length > 1) {
sequences.push(this.createWordSequenceMatch(currentSequence, password));
}
return sequences;
}
isValidWordSequence(currentSequence, nextMatch, password) {
// Get the text between the last word and the next word
const lastWord = currentSequence[currentSequence.length - 1];
const textBetween = password.slice(lastWord.j + 1, nextMatch.i);
// Check for common word separators
const separators = ['', ' ', '-', '_', '.', ''];
// For simple concatenation (no separator)
const isSimpleConcat = textBetween === '';
// For snake_case or kebab-case, check for separators
const hasValidSeparator = separators.some(sep => textBetween === sep);
// For camelCase, we need to check if the next word starts with uppercase
// and there's no separator (or just a single character that could be uppercase)
const isCamelCase = textBetween.length === 1 && textBetween === textBetween.toUpperCase() && textBetween !== textBetween.toLowerCase();
return hasValidSeparator || isSimpleConcat || isCamelCase;
}
createWordSequenceMatch(sequence, password) {
const firstMatch = sequence[0];
const lastMatch = sequence[sequence.length - 1];
const words = sequence.map(match => match.word);
// Determine if sequence is ascending (by rank)
const ranks = sequence.map(match => match.rank);
const ascending = ranks.every((rank, i) => i === 0 || rank >= ranks[i - 1]);
// Use the most common dictionary name, or the first one
const dictionaryNames = sequence.map(match => match.dictionaryName);
const dictionaryName = this.getMostCommon(dictionaryNames) || firstMatch.dictionaryName;
return {
pattern: 'wordSequence',
i: firstMatch.i,
j: lastMatch.j,
token: password.slice(firstMatch.i, lastMatch.j + 1),
words,
wordCount: words.length,
dictionaryName,
ascending
};
}
getMostCommon(array) {
if (array.length === 0) return null;
const counts = new Map();
let maxCount = 0;
let mostCommon = null;
array.forEach(item => {
const count = (counts.get(item) || 0) + 1;
counts.set(item, count);
if (count > maxCount) {
maxCount = count;
mostCommon = item;
}
});
return mostCommon;
}
}
export { MatchWordSequence as default };
//# sourceMappingURL=matching.mjs.map