Skip to content

compat/truncate: crashes with SyntaxError when string separator contains RegExp metacharacters #2129

Closed

Description

@TalhaHunter101

es-toolkit version

1.52.0

Entry point

es-toolkit/compat

Reproduction

import { truncate } from 'es-toolkit/compat';

// 1. Quantifiers and bracket characters cause fatal syntax errors:
truncate('hello world? how are you doing today?', { length: 20, separator: '?' });
// SyntaxError: Invalid regular expression: /(?<result>.*(?:(?!?).))(?:?)/u: Nothing to repeat

truncate('section [1] details and notes', { length: 20, separator: '[' });
// SyntaxError: Invalid regular expression: /(?<result>.*(?:(?![).))(?:[)/u: Unterminated character class

truncate('part (A) overview notes', { length: 20, separator: '(' });
// SyntaxError: Invalid regular expression: /(?<result>.*(?:(?!().))(?:()/u: Unterminated group

truncate('a + b + c + d + e', { length: 10, separator: '+' });
// SyntaxError: Invalid regular expression: /(?<result>.*(?:(?!+).))(?:+)/u: Nothing to repeat

// 2. Wildcard metacharacters match unintended characters:
truncate('hello world foo bar', { length: 15, separator: '.' });
// Returns 'hello world ...' because '.' in RegExp matches any character rather than a literal period.

Current vs. expected behavior

Lodash (Expected)

Lodash accepts any string separator and treats it as a literal substring:

import _ from 'lodash';

_.truncate('hello world? how are you doing today?', { length: 20, separator: '?' });
// => "hello world?..."

_.truncate('section [1] details and notes', { length: 20, separator: '[' });
// => "section ..."

_.truncate('a + b + c + d + e', { length: 10, separator: '+' });
// => "a + b + c..."

_.truncate('hello world foo bar', { length: 15, separator: '.' });
// => "hello world ..." (doesn't cut at arbitrary letters because '.' is treated as a literal character)

es-toolkit/compat (Current)

In src/compat/string/truncate.ts (lines 84-86):

const search = separator instanceof RegExp ? separator.source : separator;
const flags = 'u' + (separator instanceof RegExp ? separator.flags.replace('u', '') : '');
const withoutSeparator = new RegExp(`(?<result>.*(?:(?!${search}).))(?:${search})`, flags).exec(base);

When separator is a string, it is directly interpolated into new RegExp(...) without escaping regex metacharacters (?, +, *, [, (, \, {, |, .). This causes:

  1. Fatal SyntaxError crashes on quantifiers and bracket characters.
  2. Wildcard matching when using . as a separator.

Root cause & Proposed fix

The string separator should be escaped using escapeRegExp(separator) before being interpolated into the RegExp. es-toolkit already provides escapeRegExp in src/string/escapeRegExp.ts.

I have isolated the fix and verified it locally with regression tests, and would be happy to open a PR for this.

Validations

  • I searched the existing issues and discussions.
  • I read the Contributing Guidelines.
  • I wrote this description myself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions