Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c31b9a5
added fix for #23
nico-martin Apr 30, 2026
1210ddf
Potential fix for pull request finding
nico-martin Apr 30, 2026
dfbc952
added warning for \W inside a character class
nico-martin Jun 8, 2026
512c940
fixed the main escaped bracket cases
nico-martin Jun 8, 2026
a40ac51
prettier fix
nico-martin Jun 8, 2026
15d3c2b
Apply suggestions from code review
xenova Jun 10, 2026
a7e0f2a
added regex test cases
nico-martin Jun 10, 2026
653cd96
added check for py regex split
nico-martin Jun 10, 2026
664db61
added section in readme
nico-martin Jun 10, 2026
82d9cfe
fixed copilot suggestions
nico-martin Jun 10, 2026
2bf49e2
removed PROBLEMATIC_REGEX_MAP and replaced it with general normalizers
nico-martin Jun 10, 2026
5ea8e7f
Merge branch 'main' into nico/regex-discrepancies-with-python-23
nico-martin Jun 10, 2026
ab6893a
merged main
nico-martin Jun 10, 2026
1574b32
improved fetchConfigById
nico-martin Jun 10, 2026
02c34db
improved fetchConfigById
nico-martin Jun 10, 2026
527134b
improved fetchConfigById
nico-martin Jun 10, 2026
4ecaf84
improved fetchConfigById
nico-martin Jun 10, 2026
d1fb055
improved fetchConfigById
nico-martin Jun 10, 2026
bf82645
Merge branch 'main' into nico/regex-discrepancies-with-python-23
xenova Jun 12, 2026
fb66bcd
Update pnpm-workspace.yaml
xenova Jun 12, 2026
607c2d6
Update tsconfig.json
xenova Jun 12, 2026
f187508
no need to use jest/globals
xenova Jun 12, 2026
8738c57
Update edgeCases.test.ts
xenova Jun 12, 2026
c9679fb
consolidate
xenova Jun 12, 2026
9e5ed6a
compile pattern once
xenova Jun 13, 2026
d7b0f6a
Update splitRegexOracle.json
xenova Jun 13, 2026
bd8e724
Update splitRegexPatterns.json
xenova Jun 13, 2026
b761789
Update generate_split_regex_oracle.py
xenova Jun 13, 2026
7640409
Update edgeCases.test.ts
xenova Jun 13, 2026
4036d5c
Delete splitRegexCases.ts
xenova Jun 13, 2026
0b95841
Improve PY -> JS regex handling
xenova Jun 13, 2026
4057f1f
increase fetch timeout
xenova Jun 13, 2026
db7600e
Update splitRegexOracle.test.ts
xenova Jun 13, 2026
f693244
Update README.md
xenova Jun 13, 2026
0319095
cleanup
xenova Jun 15, 2026
3d4b1da
Update README.md
xenova Jun 15, 2026
0c8f8f2
compress oracle
xenova Jun 15, 2026
6b1a81e
formatting
xenova Jun 15, 2026
4ee2f73
reduce size
xenova Jun 15, 2026
ae07e54
increase default test timeout
xenova Jun 15, 2026
d24f000
cleanup
xenova Jun 15, 2026
83e6bad
Merge branch 'main' into nico/regex-discrepancies-with-python-23
xenova Jun 16, 2026
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.gz binary
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests/py/*.py
*.gz
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ This library expects two files from Hugging Face models:
- `tokenizer.json` - Contains the tokenizer configuration
- `tokenizer_config.json` - Contains additional metadata

## Regex compatibility

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.

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.

## Components

Tokenizers.js supports [Hugging Face tokenizer components](https://huggingface.co/docs/tokenizers/components):
Expand Down
4 changes: 4 additions & 0 deletions jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export default {
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},

// Default timeout of a test/hook in milliseconds (raised from Jest's 5s default to allow
// for network fetches of tokenizer configs on a cold cache).
testTimeout: 15000,

// Adds a location field to test results
// testLocationInResults: false,

Expand Down
2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
allowBuilds:
esbuild: true
unrs-resolver: true

minimumReleaseAge: 1440
13 changes: 12 additions & 1 deletion src/core/decoder/Replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@ import { TokenizerConfigDecoderReplace } from "@static/tokenizer";

class Replace extends Decoder {
declare config: TokenizerConfigDecoderReplace;
pattern: RegExp | null;

/**
* @param config The configuration object for the decoder.
*/
constructor(config: TokenizerConfigDecoderReplace) {
super(config);
// Compile once: decode_chain() is called for every decode.
this.pattern = create_pattern(this.config.pattern);
}

decode_chain(tokens: string[]): string[] {
const pattern = create_pattern(this.config.pattern);
const content = this.config.content ?? "";
const pattern = this.pattern;
return pattern === null
? tokens
: tokens.map((token) => token.replaceAll(pattern, content));
Expand Down
15 changes: 12 additions & 3 deletions src/core/normalizer/Replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,26 @@ import type { TokenizerConfigNormalizerReplace } from "@static/tokenizer";
*/
class Replace extends Normalizer {
declare config: TokenizerConfigNormalizerReplace;
pattern: RegExp | null;

/**
* @param config The configuration object for the normalizer.
*/
constructor(config: TokenizerConfigNormalizerReplace) {
super(config);
// Compile once: normalize() is called for every input text.
this.pattern = create_pattern(this.config.pattern ?? {});
}

/**
* Normalize the input text by replacing the pattern with the content.
* @param text The input text to be normalized.
* @returns The normalized text after replacing the pattern with the content.
*/
normalize(text: string): string {
const pattern = create_pattern(this.config.pattern ?? {});
return pattern === null
return this.pattern === null
? text
: text.replaceAll(pattern, this.config.content ?? "");
: text.replaceAll(this.pattern, this.config.content ?? "");
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/preTokenizer/Split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Split extends PreTokenizer {
}

if (this.config.invert) {
return text.match(this.pattern) || [];
return (text.match(this.pattern) || []).filter((x) => x);
} else if (this.config.behavior?.toLowerCase() === "removed") {
return text.split(this.pattern).filter((x) => x);
} else {
Expand Down
34 changes: 0 additions & 34 deletions src/static/constants.ts
Comment thread
nico-martin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,5 @@ const reverse_dictionary = (data: Object) =>

export const UNICODE_TO_BYTES = reverse_dictionary(BYTES_TO_UNICODE);

const BLOOM_SPLIT_CHARS = ".,!?\u2026\u3002\uff0c\u3001\u0964\u06d4\u060c";

export const PROBLEMATIC_REGEX_MAP = new Map([
// These uses the case insensitive group modifier, which is not supported in JavaScript.
// When parsing the regex, an "Invalid group" error is thrown.
[
"(?i:'s|'t|'re|'ve|'m|'ll|'d)",
"(?:'([sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD]))",
],
[
"(?i:[sdmt]|ll|ve|re)",
"(?:[sS]|[dD]|[mM]|[tT]|[lL][lL]|[vV][eE]|[rR][eE])",
],

// JS doesn't support possessive quantifiers (these are used in recent OpenAI tokenizers).
["[^\\r\\n\\p{L}\\p{N}]?+", "[^\\r\\n\\p{L}\\p{N}]?"],
["[^\\s\\p{L}\\p{N}]++", "[^\\s\\p{L}\\p{N}]+"],

// JS doesn't support atomic groups (these are used in AFMoE tokenizers).
["(?>\\p{Nd}{510})", "(?:\\p{Nd}{510})"],

// JS doesn't support stacking quantifiers.
// Uncaught SyntaxError: Invalid regular expression: /\p{Nd}{3}+/u: Nothing to repeat
["\\p{Nd}{3}+", "(?:\\p{Nd}{3})+"],

// \G is an invalid escape in JS, and in most cases is just used as an optimization.
// So, we can safely remove it.
["\\G", ""],

// Used to override the default (invalid) regex of the bloom pretokenizer.
// For more information, see https://github.com/huggingface/transformers.js/issues/94
[` ?[^(\\s|[${BLOOM_SPLIT_CHARS}])]+`, ` ?[^\\s${BLOOM_SPLIT_CHARS}]+`],
]);

export const PUNCTUATION_REGEX =
"\\p{P}\\u0021-\\u002F\\u003A-\\u0040\\u005B-\\u0060\\u007B-\\u007E";
Loading
Loading