From 5e4c4c2b0096b9b60938a36ee45d8e077e0320fc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 17:49:20 +0000 Subject: [PATCH 1/3] perf: hoist regex literals to module scope Regexes in unquote(), print_url(), format_declaration(), and format_atrule_prelude() were declared as literals inside hot functions, causing repeated RegExp allocation on every call. Hoisting them to top-level constants avoids that overhead; format_atrule_prelude() in particular runs 9 regex passes per at-rule prelude. No behavior change (verified identical output across comment/atrule/url test cases and the full test suite). --- src/lib/index.ts | 51 +++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 8a11b05..f50f3df 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -58,8 +58,22 @@ export type FormatOptions = { tab_size?: number } +const UNQUOTE_RE = /(?:^['"])|(?:['"]$)/g +const DATA_URL_RE = /^['"]?data:/i +const FONT_SLASH_RE = /\s*\/\s*/ +const ATRULE_COLON_COMMA_RE = /\s*([:,])/g +const ATRULE_PAREN_TEXT_RE = /\)([a-zA-Z])/g +const ATRULE_KEYWORD_PAREN_RE = /\b(and|or|not|only)\(/gi +const ATRULE_ARROW_COMPARE_RE = /\s*(=>|>=|<=)\s*/g +const ATRULE_COMPARE_RE = /([^<>=\s])([<>])([^<>=\s])/g +const ATRULE_COMPARE_SPACED_RE = /([^<>=\s])\s+([<>])\s+([^<>=\s])/g +const ATRULE_WHITESPACE_RE = /\s+/g +const ATRULE_COLON_COMMA_SPACE_RE = /([:,]) /g +const ATRULE_CALC_RE = /calc\(\s*([^()+\-*/]+)\s*([*/+-])\s*([^()+\-*/]+)\s*\)/g +const ATRULE_FN_NAME_RE = /selector|url|supports|layer\(/gi + export function unquote(str: string): string { - return str.replaceAll(/(?:^['"])|(?:['"]$)/g, EMPTY_STRING) + return str.replaceAll(UNQUOTE_RE, EMPTY_STRING) } function print_string(str: string | number | null, quote?: '"' | "'"): string { @@ -76,7 +90,7 @@ function print_url(node: Url): string { let unquoted = unquote(value) let inner: string - if (/^['"]?data:/i.test(value)) { + if (DATA_URL_RE.test(value)) { let has_double = unquoted.includes('"') let has_single = unquoted.includes("'") if (has_double && has_single) { @@ -161,7 +175,7 @@ export function format_declaration( // Special case for `font` shorthand: remove whitespace around / if (property === 'font') { - value = value.replace(/\s*\/\s*/, '/') + value = value.replace(FONT_SLASH_RE, '/') } // Hacky: add a space in case of a `space toggle` during minification @@ -335,23 +349,20 @@ export function format_atrule_prelude( ): string { let optional_space = minify ? EMPTY_STRING : SPACE return prelude - .replaceAll(/\s*([:,])/g, prelude.toLowerCase().includes('selector(') ? '$1' : '$1 ') // force whitespace after colon or comma, except inside `selector()` - .replaceAll(/\)([a-zA-Z])/g, ') $1') // force whitespace between closing parenthesis and following text (usually and|or) - .replaceAll(/\b(and|or|not|only)\(/gi, '$1 (') // force whitespace between media/supports keywords and opening parenthesis - .replaceAll(/\s*(=>|>=|<=)\s*/g, `${optional_space}$1${optional_space}`) // add optional spacing around =>, >= and <= - .replaceAll(/([^<>=\s])([<>])([^<>=\s])/g, `$1${optional_space}$2${optional_space}$3`) // add spacing around < or > except when it's part of <=, >=, => - .replaceAll(/([^<>=\s])\s+([<>])\s+([^<>=\s])/g, `$1${optional_space}$2${optional_space}$3`) // handle spaces around < or > when they already have surrounding whitespace - .replaceAll(/\s+/g, SPACE) // collapse multiple whitespaces into one - .replaceAll(/([:,]) /g, minify ? '$1' : '$1 ') // in minify mode, remove optional spaces after : and , - .replaceAll( - /calc\(\s*([^()+\-*/]+)\s*([*/+-])\s*([^()+\-*/]+)\s*\)/g, - (_, left, operator, right) => { - // force required or optional whitespace around * and / in calc() - let space = operator === '+' || operator === '-' ? SPACE : optional_space - return `calc(${left.trim()}${space}${operator}${space}${right.trim()})` - }, - ) - .replaceAll(/selector|url|supports|layer\(/gi, (match) => match.toLowerCase()) // lowercase function names + .replaceAll(ATRULE_COLON_COMMA_RE, prelude.toLowerCase().includes('selector(') ? '$1' : '$1 ') // force whitespace after colon or comma, except inside `selector()` + .replaceAll(ATRULE_PAREN_TEXT_RE, ') $1') // force whitespace between closing parenthesis and following text (usually and|or) + .replaceAll(ATRULE_KEYWORD_PAREN_RE, '$1 (') // force whitespace between media/supports keywords and opening parenthesis + .replaceAll(ATRULE_ARROW_COMPARE_RE, `${optional_space}$1${optional_space}`) // add optional spacing around =>, >= and <= + .replaceAll(ATRULE_COMPARE_RE, `$1${optional_space}$2${optional_space}$3`) // add spacing around < or > except when it's part of <=, >=, => + .replaceAll(ATRULE_COMPARE_SPACED_RE, `$1${optional_space}$2${optional_space}$3`) // handle spaces around < or > when they already have surrounding whitespace + .replaceAll(ATRULE_WHITESPACE_RE, SPACE) // collapse multiple whitespaces into one + .replaceAll(ATRULE_COLON_COMMA_SPACE_RE, minify ? '$1' : '$1 ') // in minify mode, remove optional spaces after : and , + .replaceAll(ATRULE_CALC_RE, (_, left, operator, right) => { + // force required or optional whitespace around * and / in calc() + let space = operator === '+' || operator === '-' ? SPACE : optional_space + return `calc(${left.trim()}${space}${operator}${space}${right.trim()})` + }) + .replaceAll(ATRULE_FN_NAME_RE, (match) => match.toLowerCase()) // lowercase function names } /** From b3d4a88b2e1e75ba3c0ed4507715e35bfb075be0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 17:51:46 +0000 Subject: [PATCH 2/3] fix: resolve pre-existing lint errors (unicorn/prefer-single-call, prefer-number-coercion) Unrelated to the regex-hoisting change, but blocking CI on this PR. --- src/lib/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index f50f3df..4c58561 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -123,9 +123,7 @@ function print_list(nodes: CSSNode[], optional_space = SPACE): string { for (let node of nodes) { if (is_function(node)) { let fn = node.name.toLowerCase() - parts.push(fn, OPEN_PARENTHESES) - parts.push(print_list(node.children, optional_space)) - parts.push(CLOSE_PARENTHESES) + parts.push(fn, OPEN_PARENTHESES, print_list(node.children, optional_space), CLOSE_PARENTHESES) } else if (is_dimension(node)) { parts.push(node.value, node.unit?.toLowerCase()) } else if (is_string(node)) { @@ -198,8 +196,8 @@ function print_nth(node: NthSelector, optional_space = SPACE): string { result += optional_space if (!b.startsWith('-')) result += '+' + optional_space } - // the parseFloat removes the leading '+', if present - result += parseFloat(b) + // Number() removes the leading '+', if present + result += Number(b) } return result } From 64f4006034870c3fbec70acf2381d6b22323d6c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 18:13:33 +0000 Subject: [PATCH 3/3] fix: revert Number(b) back to parseFloat(b) in print_nth Number() breaks formatting that parseFloat's lenient trailing-character handling relied on. Suppress the linter rule instead of changing behavior. --- src/lib/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 4c58561..b12b02c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -196,8 +196,9 @@ function print_nth(node: NthSelector, optional_space = SPACE): string { result += optional_space if (!b.startsWith('-')) result += '+' + optional_space } - // Number() removes the leading '+', if present - result += Number(b) + // parseFloat tolerates trailing non-numeric characters that Number() would reject as NaN + // oxlint-disable-next-line unicorn/prefer-number-coercion + result += parseFloat(b) } return result }