Skip to content

Commit af45330

Browse files
authored
Merge pull request #24 from huggingface/nico/regex-discrepancies-with-python-23
Improve robustness for handling Python-authored regexes
2 parents 3029328 + 83e6bad commit af45330

17 files changed

Lines changed: 1476 additions & 69 deletions

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.gz binary

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tests/py/*.py
2+
*.gz

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ This library expects two files from Hugging Face models:
6464
- `tokenizer.json` - Contains the tokenizer configuration
6565
- `tokenizer_config.json` - Contains additional metadata
6666

67+
## Regex compatibility
68+
69+
Tokenizer configs are authored for the Rust `tokenizers` crate, which compiles patterns with Oniguruma — a regex engine whose syntax and Unicode semantics differ from JavaScript `RegExp`. Tokenizers.js translates these patterns structurally, matching Oniguruma behavior for: line anchors (`^`/`$` recognize only `\n`, unlike JavaScript's `m` flag), absolute anchors (`\A`, `\z`, `\Z`), `.` (which excludes only `\n`), word/digit/space shorthands (including Oniguruma's exact word-character set and its Latin-1 ctype quirks), `\h`/`\H` hex digits, `\b`/`\B` boundaries, inline case-insensitive groups (including ranges like `(?i:[a-f])`), possessive and stacked quantifiers (`a++`, `X{3}+`), atomic groups, POSIX bracket expressions, `\x{...}` code points, `\p{Word}`, identity escapes, and literal braces/brackets.
70+
71+
A few constructs can't be fully reproduced — notably `\G`, full Unicode case folding (e.g. `ß` ~ `ss`), character-class intersection (`&&`), and the `MergedWithPrevious`/`MergedWithNext`/`Contiguous` split behaviors.
72+
6773
## Components
6874

6975
Tokenizers.js supports [Hugging Face tokenizer components](https://huggingface.co/docs/tokenizers/components):

jest.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export default {
139139
// Options that will be passed to the testEnvironment
140140
// testEnvironmentOptions: {},
141141

142+
// Default timeout of a test/hook in milliseconds (raised from Jest's 5s default to allow
143+
// for network fetches of tokenizer configs on a cold cache).
144+
testTimeout: 15000,
145+
142146
// Adds a location field to test results
143147
// testLocationInResults: false,
144148

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
allowBuilds:
22
esbuild: true
33
unrs-resolver: true
4+
5+
minimumReleaseAge: 1440

src/core/decoder/Replace.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ import { TokenizerConfigDecoderReplace } from "@static/tokenizer";
44

55
class Replace extends Decoder {
66
declare config: TokenizerConfigDecoderReplace;
7+
pattern: RegExp | null;
8+
9+
/**
10+
* @param config The configuration object for the decoder.
11+
*/
12+
constructor(config: TokenizerConfigDecoderReplace) {
13+
super(config);
14+
// Compile once: decode_chain() is called for every decode.
15+
this.pattern = create_pattern(this.config.pattern);
16+
}
17+
718
decode_chain(tokens: string[]): string[] {
8-
const pattern = create_pattern(this.config.pattern);
919
const content = this.config.content ?? "";
20+
const pattern = this.pattern;
1021
return pattern === null
1122
? tokens
1223
: tokens.map((token) => token.replaceAll(pattern, content));

src/core/normalizer/Replace.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,26 @@ import type { TokenizerConfigNormalizerReplace } from "@static/tokenizer";
88
*/
99
class Replace extends Normalizer {
1010
declare config: TokenizerConfigNormalizerReplace;
11+
pattern: RegExp | null;
12+
13+
/**
14+
* @param config The configuration object for the normalizer.
15+
*/
16+
constructor(config: TokenizerConfigNormalizerReplace) {
17+
super(config);
18+
// Compile once: normalize() is called for every input text.
19+
this.pattern = create_pattern(this.config.pattern ?? {});
20+
}
1121

1222
/**
1323
* Normalize the input text by replacing the pattern with the content.
1424
* @param text The input text to be normalized.
1525
* @returns The normalized text after replacing the pattern with the content.
1626
*/
1727
normalize(text: string): string {
18-
const pattern = create_pattern(this.config.pattern ?? {});
19-
return pattern === null
28+
return this.pattern === null
2029
? text
21-
: text.replaceAll(pattern, this.config.content ?? "");
30+
: text.replaceAll(this.pattern, this.config.content ?? "");
2231
}
2332
}
2433

src/core/preTokenizer/Split.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Split extends PreTokenizer {
3535
}
3636

3737
if (this.config.invert) {
38-
return text.match(this.pattern) || [];
38+
return (text.match(this.pattern) || []).filter((x) => x);
3939
} else if (this.config.behavior?.toLowerCase() === "removed") {
4040
return text.split(this.pattern).filter((x) => x);
4141
} else {

src/static/constants.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,39 +35,5 @@ const reverse_dictionary = (data: Object) =>
3535

3636
export const UNICODE_TO_BYTES = reverse_dictionary(BYTES_TO_UNICODE);
3737

38-
const BLOOM_SPLIT_CHARS = ".,!?\u2026\u3002\uff0c\u3001\u0964\u06d4\u060c";
39-
40-
export const PROBLEMATIC_REGEX_MAP = new Map([
41-
// These uses the case insensitive group modifier, which is not supported in JavaScript.
42-
// When parsing the regex, an "Invalid group" error is thrown.
43-
[
44-
"(?i:'s|'t|'re|'ve|'m|'ll|'d)",
45-
"(?:'([sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD]))",
46-
],
47-
[
48-
"(?i:[sdmt]|ll|ve|re)",
49-
"(?:[sS]|[dD]|[mM]|[tT]|[lL][lL]|[vV][eE]|[rR][eE])",
50-
],
51-
52-
// JS doesn't support possessive quantifiers (these are used in recent OpenAI tokenizers).
53-
["[^\\r\\n\\p{L}\\p{N}]?+", "[^\\r\\n\\p{L}\\p{N}]?"],
54-
["[^\\s\\p{L}\\p{N}]++", "[^\\s\\p{L}\\p{N}]+"],
55-
56-
// JS doesn't support atomic groups (these are used in AFMoE tokenizers).
57-
["(?>\\p{Nd}{510})", "(?:\\p{Nd}{510})"],
58-
59-
// JS doesn't support stacking quantifiers.
60-
// Uncaught SyntaxError: Invalid regular expression: /\p{Nd}{3}+/u: Nothing to repeat
61-
["\\p{Nd}{3}+", "(?:\\p{Nd}{3})+"],
62-
63-
// \G is an invalid escape in JS, and in most cases is just used as an optimization.
64-
// So, we can safely remove it.
65-
["\\G", ""],
66-
67-
// Used to override the default (invalid) regex of the bloom pretokenizer.
68-
// For more information, see https://github.com/huggingface/transformers.js/issues/94
69-
[` ?[^(\\s|[${BLOOM_SPLIT_CHARS}])]+`, ` ?[^\\s${BLOOM_SPLIT_CHARS}]+`],
70-
]);
71-
7238
export const PUNCTUATION_REGEX =
7339
"\\p{P}\\u0021-\\u002F\\u003A-\\u0040\\u005B-\\u0060\\u007B-\\u007E";

0 commit comments

Comments
 (0)