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