feat(ext/shared): add colorizePassword utility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
60
extension/src/shared/__tests__/password-coloring.test.ts
Normal file
60
extension/src/shared/__tests__/password-coloring.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { colorizePassword, PWD_DIGIT, PWD_SYMBOL, PWD_LETTER } from '../password-coloring';
|
||||
|
||||
describe('colorizePassword', () => {
|
||||
|
||||
function classes(frag: DocumentFragment): string[] {
|
||||
return Array.from(frag.querySelectorAll('span')).map(s => s.className);
|
||||
}
|
||||
function texts(frag: DocumentFragment): string[] {
|
||||
return Array.from(frag.querySelectorAll('span')).map(s => s.textContent ?? '');
|
||||
}
|
||||
|
||||
it('returns empty fragment for empty input', () => {
|
||||
const frag = colorizePassword('');
|
||||
expect(frag.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('classifies a mixed-class run', () => {
|
||||
const frag = colorizePassword('aB3$xY');
|
||||
expect(classes(frag)).toEqual([PWD_LETTER, PWD_DIGIT, PWD_SYMBOL, PWD_LETTER]);
|
||||
expect(texts(frag)).toEqual(['aB', '3', '$', 'xY']);
|
||||
});
|
||||
|
||||
it('all-letters produces a single letter span', () => {
|
||||
const frag = colorizePassword('passwd');
|
||||
expect(classes(frag)).toEqual([PWD_LETTER]);
|
||||
expect(texts(frag)).toEqual(['passwd']);
|
||||
});
|
||||
|
||||
it('all-digits produces a single digit span', () => {
|
||||
const frag = colorizePassword('123456');
|
||||
expect(classes(frag)).toEqual([PWD_DIGIT]);
|
||||
expect(texts(frag)).toEqual(['123456']);
|
||||
});
|
||||
|
||||
it('all-symbols produces a single symbol span', () => {
|
||||
const frag = colorizePassword('!@#$%^');
|
||||
expect(classes(frag)).toEqual([PWD_SYMBOL]);
|
||||
expect(texts(frag)).toEqual(['!@#$%^']);
|
||||
});
|
||||
|
||||
it('classifies unicode letters as letters', () => {
|
||||
const frag = colorizePassword('áñü');
|
||||
expect(classes(frag)).toEqual([PWD_LETTER]);
|
||||
});
|
||||
|
||||
it('classifies whitespace as symbol', () => {
|
||||
const frag = colorizePassword('a b');
|
||||
expect(classes(frag)).toEqual([PWD_LETTER, PWD_SYMBOL, PWD_LETTER]);
|
||||
expect(texts(frag)).toEqual(['a', ' ', 'b']);
|
||||
});
|
||||
|
||||
it('representative password snapshot: aB3$xY7&_!', () => {
|
||||
const frag = colorizePassword('aB3$xY7&_!');
|
||||
expect(classes(frag)).toEqual([
|
||||
PWD_LETTER, PWD_DIGIT, PWD_SYMBOL, PWD_LETTER, PWD_DIGIT, PWD_SYMBOL,
|
||||
]);
|
||||
expect(texts(frag)).toEqual(['aB', '3', '$', 'xY', '7', '&_!']);
|
||||
});
|
||||
});
|
||||
35
extension/src/shared/password-coloring.ts
Normal file
35
extension/src/shared/password-coloring.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export const PWD_DIGIT = 'pwd-digit';
|
||||
export const PWD_SYMBOL = 'pwd-symbol';
|
||||
export const PWD_LETTER = 'pwd-letter';
|
||||
|
||||
type Class = typeof PWD_DIGIT | typeof PWD_SYMBOL | typeof PWD_LETTER;
|
||||
|
||||
function classify(ch: string): Class {
|
||||
if (/^\d$/.test(ch)) return PWD_DIGIT;
|
||||
if (/^\p{L}$/u.test(ch)) return PWD_LETTER;
|
||||
return PWD_SYMBOL;
|
||||
}
|
||||
|
||||
export function colorizePassword(text: string): DocumentFragment {
|
||||
const frag = document.createDocumentFragment();
|
||||
if (text.length === 0) return frag;
|
||||
|
||||
const codepoints = Array.from(text);
|
||||
let runStart = 0;
|
||||
let runClass = classify(codepoints[0]);
|
||||
|
||||
for (let i = 1; i <= codepoints.length; i++) {
|
||||
const c = i < codepoints.length ? classify(codepoints[i]) : null;
|
||||
if (c !== runClass) {
|
||||
const span = document.createElement('span');
|
||||
span.className = runClass;
|
||||
span.textContent = codepoints.slice(runStart, i).join('');
|
||||
frag.appendChild(span);
|
||||
if (c !== null) {
|
||||
runStart = i;
|
||||
runClass = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
Reference in New Issue
Block a user