feat(ext/shared): add colorizePassword utility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-02 17:17:05 -04:00
parent 25c9eb52a0
commit 1de7cda1b0
2 changed files with 95 additions and 0 deletions

View 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', '&_!']);
});
});

View 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;
}