Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions scripts/benchmark-profanity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ function printResults(results) {
const filter = createProfanityFilter();
const scanner = createProfanityScanner({ filter });
const input = (text) => ({ text, codePoints: Array.from(text) });
const hintedEmptyInput = {
text: "",
codePoints: [],
hints: {
textLength: 0,
codePointLength: 0,
isEmpty: true,
hasAsciiOnly: true,
hasNonAscii: false,
},
};

printResults([
bench(
Expand All @@ -58,6 +69,7 @@ printResults([
bench("check loose match", () => filter.check(LOOSE_MATCH)),
bench("check long late match", () => filter.check(LONG_LATE_MATCH)),
bench("scanner check short clean", () => scanner.check(input(SHORT_CLEAN))),
bench("scanner check hinted empty", () => scanner.check(hintedEmptyInput)),
bench("scanner check short match", () => scanner.check(input(SHORT_MATCH))),
bench("scanner scan short match", () => scanner.scan(input(SHORT_MATCH))),
bench("analyze short match", () => filter.analyze(SHORT_MATCH)),
Expand Down
5 changes: 4 additions & 1 deletion src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export function createProfanityScanner(

const scanner: ProfanityScanner = {
name: PROFANITY_FILTER_NAME,
check: (input) => activeFilter.check(input.text, matchOptions),
check: (input) =>
allocationAware && input.hints?.textLength === 0
? false
: activeFilter.check(input.text, matchOptions),
scan,
};

Expand Down
24 changes: 20 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,26 @@ export interface ProfanityFilter extends ReadonlyProfanityFilter {
export interface ProfanityScanInput {
readonly text: string;
readonly codePoints: readonly string[];
readonly hints?: {
readonly textLength?: number;
readonly hasNonAscii?: boolean;
};
readonly hints?: ProfanityScanHints;
}

export interface ProfanityScanHints {
readonly textLength?: number;
readonly codePointLength?: number;
readonly isEmpty?: boolean;
readonly hasAsciiOnly?: boolean;
readonly hasNonAscii?: boolean;
readonly hasDigit?: boolean;
readonly digitCount?: number;
readonly hasAsciiLetter?: boolean;
readonly hasWhitespace?: boolean;
readonly hasPunctuation?: boolean;
readonly punctuationCount?: number;
readonly hasAtSign?: boolean;
readonly hasDot?: boolean;
readonly hasSlash?: boolean;
readonly hasColon?: boolean;
readonly hasPlus?: boolean;
}

export interface ProfanityScannerOptions {
Expand Down
26 changes: 25 additions & 1 deletion tests/scanner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ describe("profanity scanner adapter", () => {
).toBe(false);
});

it("uses shared text-length hints to skip package-owned empty checks", () => {
const filter = createProfanityFilter(["alpha"], []);
const checkSpy = vi.spyOn(filter, "check");
const scanner = createProfanityScanner({ filter });

expect(
scanner.check({
text: "",
codePoints: [],
hints: { textLength: 0, hasNonAscii: false },
}),
).toBe(false);
expect(checkSpy).not.toHaveBeenCalled();
});

it("streams ranges into a sink and supports early stop", () => {
const filter = createProfanityFilter(["alpha", "beta"], []);
const scanner = createProfanityScanner({ filter });
Expand Down Expand Up @@ -108,17 +123,26 @@ describe("profanity scanner adapter", () => {
const analyze = vi.fn(() => [
Object.assign([0, 5] as [number, number], { mode: "strict" as const }),
]);
const check = vi.fn(() => true);
const scanner = createProfanityScanner({
filter: {
name: PROFANITY_FILTER_NAME,
analyze,
check: () => true,
check,
censor: (text) => String(text),
},
});
const seen: TextCodePointRange[] = [];

expect("allocationAware" in scanner).toBe(false);
expect(
scanner.check({
text: "",
codePoints: [],
hints: { textLength: 0, hasNonAscii: false },
}),
).toBe(true);
expect(check).toHaveBeenCalledOnce();
expect(
scanner.scan(
{ text: "alpha", codePoints: Array.from("alpha") },
Expand Down