feat: hook up validators with input fields

This commit is contained in:
tdgao
2026-08-24 16:19:49 -07:00
committed by Prospector
parent e618c1625a
commit eb22882138
12 changed files with 156 additions and 23 deletions
@@ -47,6 +47,7 @@ const PRIVATE_USE_PATTERN = /\p{Co}/u
const UNASSIGNED_PATTERN = /\p{Cn}/u
const LETTER_PATTERN = /\p{L}/u
const EXTENDED_PICTOGRAPHIC_PATTERN = /\p{Extended_Pictographic}/u
const EMOJI_PRESENTATION_PATTERN = /\p{Emoji_Presentation}/u
const UNIFIED_IDEOGRAPH_PATTERN = /\p{Unified_Ideograph}/u
function createCounts(): Record<NonStandardTextIssueKind, number> {
@@ -75,6 +76,10 @@ function isEmojiModifier(codePoint: number) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff
}
function isEmojiTag(codePoint: number) {
return codePoint >= 0xe0020 && codePoint <= 0xe007f
}
function isAscii(character: string) {
return character.codePointAt(0)! <= 0x7f
}
@@ -129,6 +134,24 @@ function isAllowedVariationSelector(
return UNIFIED_IDEOGRAPH_PATTERN.test(previous)
}
function isAllowedEmojiTagSequence(characters: readonly string[], characterIndex: number) {
let start = characterIndex - 1
while (start >= 0 && isEmojiTag(characters[start].codePointAt(0)!)) start--
if (characters[start]?.codePointAt(0) !== 0x1f3f4) return false
let end = characterIndex
while (end < characters.length && isEmojiTag(characters[end].codePointAt(0)!)) end++
return characters[end - 1]?.codePointAt(0) === 0xe007f
}
function isPresentedAsEmoji(characters: readonly string[], characterIndex: number) {
const character = characters[characterIndex]
return (
EMOJI_PRESENTATION_PATTERN.test(character) ||
characters[characterIndex + 1]?.codePointAt(0) === 0xfe0f
)
}
function codePointLabel(codePoint: number) {
return `U+${codePoint.toString(16).toUpperCase().padStart(4, '0')}`
}
@@ -200,7 +223,8 @@ export function validateNonStandardText(
if (FORMAT_PATTERN.test(character)) {
const allowed =
(codePoint === 0x200c && isAllowedZeroWidthNonJoiner(characters, characterIndex)) ||
(codePoint === 0x200d && isAllowedZeroWidthJoiner(characters, characterIndex))
(codePoint === 0x200d && isAllowedZeroWidthJoiner(characters, characterIndex)) ||
(isEmojiTag(codePoint) && isAllowedEmojiTagSequence(characters, characterIndex))
if (!allowed) addIssue('invisible', character, codePoint, currentIndex)
hasBaseCharacter = false
combiningMarkCount = 0
@@ -225,7 +249,7 @@ export function validateNonStandardText(
combiningMarkCount = 0
hasBaseCharacter = !/^\s$/u.test(character)
if (isInRanges(codePoint, FANCY_RANGES)) {
if (isInRanges(codePoint, FANCY_RANGES) && !isPresentedAsEmoji(characters, characterIndex)) {
addIssue('fancy', character, codePoint, currentIndex)
}
}
@@ -57,6 +57,9 @@ test('allows ordinary emoji and valid emoji joiner sequences', () => {
assert.equal(validateNonStandardText('Family: 👨‍👩‍👧‍👦').valid, true)
assert.equal(validateNonStandardText('Developer: 🧑🏽‍💻').valid, true)
assert.equal(validateNonStandardText('Heart: ❤️').valid, true)
assert.equal(validateNonStandardText('Information: ️').valid, true)
assert.equal(validateNonStandardText('A button: 🅰️').valid, true)
assert.equal(validateNonStandardText('Scotland: 🏴󠁧󠁢󠁳󠁣󠁴󠁿').valid, true)
})
test('detects suspicious invisible and directional characters', () => {
@@ -499,7 +499,7 @@ function findAt(root: TrieNode, text: string, start: number): ProfanityMatch | u
const matchesNegative = current.negatives.some((negative) =>
negativeMatches(negative, text, start, end),
)
if (matchesNegative && current.children.size === 0) return undefined
if (matchesNegative) continue
return {
kind: current.terminal.kind,
@@ -57,7 +57,7 @@ test('uses the first terminal when one configured term prefixes another', () =>
})
test('does not match configured false-positive substrings', () => {
assert.equal(validateProfanity('Scunthorpe and peacock').valid, true)
assert.equal(validateProfanity('Scunthorpe, Clitheroe, and peacock').valid, true)
assert.equal(validateProfanity('cock and cunt').profanityCount, 2)
})