From ce7b892c7d5c18b92f8cd3b1974521a2e0d00665 Mon Sep 17 00:00:00 2001 From: alyldas Date: Wed, 1 Jul 2026 16:59:11 +0300 Subject: [PATCH] Align URL scanner contract --- src/contracts.ts | 22 ++++++++++++---------- src/index.ts | 18 +++--------------- tests/index.spec.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/contracts.ts b/src/contracts.ts index 74be8d8..de0b178 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -14,24 +14,26 @@ export interface UrlFilter { censor(text: unknown): string; } +export interface UrlScanHints { + readonly hasNonAscii?: boolean; + readonly hasDot?: boolean; + readonly hasSlash?: boolean; + readonly hasColon?: boolean; +} + export interface UrlScanInput { readonly text: string; readonly codePoints: readonly string[]; - readonly hints?: { - readonly hasNonAscii?: boolean; - readonly hasDot?: boolean; - readonly hasSlash?: boolean; - readonly hasColon?: boolean; - }; + readonly hints?: UrlScanHints; } -export interface UrlRangeScanResult { +export type UrlRangeScanResult = { readonly ranges: readonly TextCodePointRange[]; -} +}; -export interface UrlRangeMatch { +export type UrlRangeMatch = { readonly range: TextCodePointRange; -} +}; export type UrlRangeMatchSink = (match: UrlRangeMatch) => boolean | void; diff --git a/src/index.ts b/src/index.ts index c587d54..93e8a01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,4 @@ -import { - maskCodePointRangesPreservingLength, - normalizeTextInput, - type TextCodePointRange, -} from "@textfilters/core"; +import { censorCodePointRanges, normalizeTextInput } from "@textfilters/core"; import { URL_FILTER_NAME, @@ -20,6 +16,7 @@ export { type UrlRangeMatchSink, type UrlRangeScanner, type UrlRangeScanResult, + type UrlScanHints, type UrlScanInput, } from "./contracts.js"; export { @@ -30,15 +27,6 @@ export { type UrlScannerConfig, } from "./scanner.js"; -const maskUrlRanges = ( - codePoints: readonly string[], - ranges: readonly TextCodePointRange[], - maskChar: string, -): string => { - if (ranges.length === 0) return codePoints.join(""); - return maskCodePointRangesPreservingLength(codePoints, ranges, maskChar); -}; - export function createUrlFilter(config: UrlFilterConfig = {}): UrlFilter { const scanner = createUrlScanner({ tlds: normalizeTlds(config.tlds) }); const maskChar = config.maskChar ?? "*"; @@ -50,7 +38,7 @@ export function createUrlFilter(config: UrlFilterConfig = {}): UrlFilter { if (!source) return source; const codePoints = Array.from(source); const ranges = scanner.scan({ text: source, codePoints }).ranges; - return maskUrlRanges(codePoints, ranges, maskChar); + return censorCodePointRanges(codePoints, ranges, maskChar); }, }; } diff --git a/tests/index.spec.ts b/tests/index.spec.ts index ccb255f..3b4c967 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -8,6 +8,9 @@ import { scanUrlRangeMatches, scanUrlRanges, URL_FILTER_NAME, + type UrlRangeScanner, + type UrlRangeScanResult, + type UrlScanHints, urlFilter, } from "../src/index.js"; @@ -334,6 +337,24 @@ describe("compatibility behavior", () => { }); describe("URL scanner", () => { + it("keeps scanner contracts compatible with shared range shapes", () => { + const scanner: UrlRangeScanner = createUrlScanner(); + const hints: UrlScanHints = { + hasNonAscii: false, + hasDot: true, + hasSlash: false, + hasColon: false, + }; + const text = "visit example.com now"; + const result: UrlRangeScanResult = scanner.scan({ + text, + codePoints: Array.from(text), + hints, + }); + + expect(result).toEqual({ ranges: [[6, 17]] }); + }); + it("exposes scanner ranges compatible with code point masking", () => { const scanner = createUrlScanner(); expect( @@ -346,6 +367,20 @@ describe("URL scanner", () => { }); }); + it("keeps the public censor wrapper aligned with scanner ranges", () => { + const text = "go https://example.com/path now"; + const scanner = createUrlScanner(); + const ranges = scanner.scan({ + text, + codePoints: Array.from(text), + }).ranges; + + expect(ranges).toEqual([[3, 27]]); + expect(createUrlFilter({ maskChar: "#" }).censor(text)).toBe( + `go ${mask("https://example.com/path", "#")} now`, + ); + }); + it("checks URL candidates without collecting every range", () => { const scanner = createUrlScanner(); const text = "visit https://example.com and https://second.example now";