From 4454fa942db6cd96446725f5e014992f83de8ccc Mon Sep 17 00:00:00 2001 From: "Willow (GHOST)" Date: Thu, 12 Feb 2026 18:25:29 +0000 Subject: [PATCH 1/3] fix: use regexp.escape --- app/composables/useMarkdown.ts | 4 +--- app/composables/useStructuredFilters.ts | 4 +++- global.d.ts | 9 +++++++++ knip.ts | 1 + nuxt.config.ts | 8 ++++---- shared/utils/emoji.ts | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 global.d.ts diff --git a/app/composables/useMarkdown.ts b/app/composables/useMarkdown.ts index 4f3cfe730..e2553202d 100644 --- a/app/composables/useMarkdown.ts +++ b/app/composables/useMarkdown.ts @@ -45,10 +45,8 @@ function stripAndEscapeHtml(text: string, packageName?: string): string { stripped = stripped.trim() // Collapse multiple whitespace into single space stripped = stripped.replace(/\s+/g, ' ') - // Escape special regex characters in package name - const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Match package name at the start, optionally followed by: space, dash, colon, hyphen, or just space - const namePattern = new RegExp(`^${escapedName}\\s*[-:—]?\\s*`, 'i') + const namePattern = new RegExp(`^${RegExp.escape(packageName)}\\s*[-:—]?\\s*`, 'i') stripped = stripped.replace(namePattern, '').trim() } diff --git a/app/composables/useStructuredFilters.ts b/app/composables/useStructuredFilters.ts index a33d4d150..0f2b53fb3 100644 --- a/app/composables/useStructuredFilters.ts +++ b/app/composables/useStructuredFilters.ts @@ -412,7 +412,9 @@ export function useStructuredFilters(options: UseStructuredFiltersOptions) { function removeKeyword(keyword: string) { filters.value.keywords = filters.value.keywords.filter(k => k !== keyword) - const newQ = searchQuery.value.replace(new RegExp(`keyword:${keyword}($| )`, 'g'), '').trim() + const newQ = searchQuery.value + .replace(new RegExp(`keyword:${RegExp.escape(keyword)}($| )`, 'g'), '') + .trim() router.replace({ query: { ...route.query, q: newQ || undefined } }) if (searchQueryModel) searchQueryModel.value = newQ } diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 000000000..f369cd6ca --- /dev/null +++ b/global.d.ts @@ -0,0 +1,9 @@ +declare global { + interface RegExpConstructor { + escape(str: string): string + } +} + +// required for the global type to work +// oxlint-disable-next-line eslint-plugin-unicorn(require-module-specifiers) +export {} diff --git a/knip.ts b/knip.ts index 257a58608..7abe997d2 100644 --- a/knip.ts +++ b/knip.ts @@ -55,6 +55,7 @@ const config: KnipConfig = { 'h3-next', ], ignoreUnresolved: ['#components', '#oauth/config'], + ignoreFiles: ['global.d.ts'], }, 'cli': { project: ['src/**/*.ts!', '!src/mock-*.ts'], diff --git a/nuxt.config.ts b/nuxt.config.ts index 47a66a94c..0cf0bbf49 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -203,7 +203,7 @@ export default defineNuxtConfig({ }, typescript: { tsConfig: { - include: ['../test/unit/server/**/*.ts'], + include: ['../test/unit/server/**/*.ts', '../global.d.ts'], }, }, }, @@ -287,10 +287,10 @@ export default defineNuxtConfig({ '#cli/*': ['../cli/src/*'], }, }, - include: ['../test/unit/app/**/*.ts'], + include: ['../test/unit/app/**/*.ts', '../global.d.ts'], }, sharedTsConfig: { - include: ['../test/unit/shared/**/*.ts'], + include: ['../test/unit/shared/**/*.ts', '../global.d.ts'], }, nodeTsConfig: { compilerOptions: { @@ -301,7 +301,7 @@ export default defineNuxtConfig({ '#shared/*': ['../shared/*'], }, }, - include: ['../*.ts', '../test/e2e/**/*.ts'], + include: ['../*.ts', '../test/e2e/**/*.ts', '../global.d.ts'], }, }, diff --git a/shared/utils/emoji.ts b/shared/utils/emoji.ts index b61730922..09d7f9764 100644 --- a/shared/utils/emoji.ts +++ b/shared/utils/emoji.ts @@ -1907,7 +1907,7 @@ const emojis = { const emojisKeysRegex = new RegExp( Object.keys(emojis) - .map(key => `:${key}:`) + .map(key => `:${RegExp.escape(key)}:`) .join('|'), 'g', ) From 51be2ae9958f71b355bd64a92b65a9c41f786b28 Mon Sep 17 00:00:00 2001 From: "Willow (GHOST)" Date: Fri, 13 Feb 2026 15:14:52 +0000 Subject: [PATCH 2/3] refactor: update to use @li/regexp-escape-polyfill --- app/composables/useMarkdown.ts | 3 ++- app/composables/useStructuredFilters.ts | 3 ++- global.d.ts | 9 --------- knip.ts | 1 - nuxt.config.ts | 8 ++++---- package.json | 1 + pnpm-lock.yaml | 8 ++++++++ shared/utils/emoji.ts | 2 +- 8 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 global.d.ts diff --git a/app/composables/useMarkdown.ts b/app/composables/useMarkdown.ts index e2553202d..2af91f129 100644 --- a/app/composables/useMarkdown.ts +++ b/app/composables/useMarkdown.ts @@ -1,3 +1,4 @@ +import { regExpEscape } from '@li/regexp-escape-polyfill' import { decodeHtmlEntities } from '~/utils/formatters' interface UseMarkdownOptions { @@ -46,7 +47,7 @@ function stripAndEscapeHtml(text: string, packageName?: string): string { // Collapse multiple whitespace into single space stripped = stripped.replace(/\s+/g, ' ') // Match package name at the start, optionally followed by: space, dash, colon, hyphen, or just space - const namePattern = new RegExp(`^${RegExp.escape(packageName)}\\s*[-:—]?\\s*`, 'i') + const namePattern = new RegExp(`^${regExpEscape(packageName)}\\s*[-:—]?\\s*`, 'i') stripped = stripped.replace(namePattern, '').trim() } diff --git a/app/composables/useStructuredFilters.ts b/app/composables/useStructuredFilters.ts index 0f2b53fb3..bba08667c 100644 --- a/app/composables/useStructuredFilters.ts +++ b/app/composables/useStructuredFilters.ts @@ -17,6 +17,7 @@ import { parseSortOption, UPDATED_WITHIN_OPTIONS, } from '#shared/types/preferences' +import { regExpEscape } from '@li/regexp-escape-polyfill' /** * Parsed search operators from text input @@ -413,7 +414,7 @@ export function useStructuredFilters(options: UseStructuredFiltersOptions) { function removeKeyword(keyword: string) { filters.value.keywords = filters.value.keywords.filter(k => k !== keyword) const newQ = searchQuery.value - .replace(new RegExp(`keyword:${RegExp.escape(keyword)}($| )`, 'g'), '') + .replace(new RegExp(`keyword:${regExpEscape(keyword)}($| )`, 'g'), '') .trim() router.replace({ query: { ...route.query, q: newQ || undefined } }) if (searchQueryModel) searchQueryModel.value = newQ diff --git a/global.d.ts b/global.d.ts deleted file mode 100644 index f369cd6ca..000000000 --- a/global.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare global { - interface RegExpConstructor { - escape(str: string): string - } -} - -// required for the global type to work -// oxlint-disable-next-line eslint-plugin-unicorn(require-module-specifiers) -export {} diff --git a/knip.ts b/knip.ts index 7abe997d2..257a58608 100644 --- a/knip.ts +++ b/knip.ts @@ -55,7 +55,6 @@ const config: KnipConfig = { 'h3-next', ], ignoreUnresolved: ['#components', '#oauth/config'], - ignoreFiles: ['global.d.ts'], }, 'cli': { project: ['src/**/*.ts!', '!src/mock-*.ts'], diff --git a/nuxt.config.ts b/nuxt.config.ts index 0cf0bbf49..47a66a94c 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -203,7 +203,7 @@ export default defineNuxtConfig({ }, typescript: { tsConfig: { - include: ['../test/unit/server/**/*.ts', '../global.d.ts'], + include: ['../test/unit/server/**/*.ts'], }, }, }, @@ -287,10 +287,10 @@ export default defineNuxtConfig({ '#cli/*': ['../cli/src/*'], }, }, - include: ['../test/unit/app/**/*.ts', '../global.d.ts'], + include: ['../test/unit/app/**/*.ts'], }, sharedTsConfig: { - include: ['../test/unit/shared/**/*.ts', '../global.d.ts'], + include: ['../test/unit/shared/**/*.ts'], }, nodeTsConfig: { compilerOptions: { @@ -301,7 +301,7 @@ export default defineNuxtConfig({ '#shared/*': ['../shared/*'], }, }, - include: ['../*.ts', '../test/e2e/**/*.ts', '../global.d.ts'], + include: ['../*.ts', '../test/e2e/**/*.ts'], }, }, diff --git a/package.json b/package.json index a8dac7839..d888afcca 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@iconify-json/svg-spinners": "1.2.4", "@iconify-json/vscode-icons": "1.2.40", "@intlify/shared": "11.2.8", + "@li/regexp-escape-polyfill": "jsr:0.3.4", "@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@f07e1a3", "@nuxt/a11y": "1.0.0-alpha.1", "@nuxt/fonts": "0.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52a4afe68..7665f4ccb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@intlify/shared': specifier: 11.2.8 version: 11.2.8 + '@li/regexp-escape-polyfill': + specifier: jsr:0.3.4 + version: '@jsr/li__regexp-escape-polyfill@0.3.4' '@lunariajs/core': specifier: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@f07e1a3 version: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@f07e1a3 @@ -1857,6 +1860,9 @@ packages: '@jsr/deno__graph@0.86.9': resolution: {integrity: sha512-+qrrma5/bL+hcG20mfaEeC8SLopqoyd1RjcKFMRu++3SAXyrTKuvuIjBJCn/NyN7X+kV+QrJG67BCHX38Rzw+g==, tarball: https://npm.jsr.io/~/11/@jsr/deno__graph/0.86.9.tgz} + '@jsr/li__regexp-escape-polyfill@0.3.4': + resolution: {integrity: sha512-UGHzM9zAlRt0DUQIgsXAuXBd9jdzJ3t1v+fHsIcIj2fbUKplO9A8v2FE/w9KZo8hs79n5IamuBvqCTndGADM2Q==, tarball: https://npm.jsr.io/~/11/@jsr/li__regexp-escape-polyfill/0.3.4.tgz} + '@jsr/std__bytes@1.0.6': resolution: {integrity: sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz} @@ -11425,6 +11431,8 @@ snapshots: '@jsr/deno__graph@0.86.9': {} + '@jsr/li__regexp-escape-polyfill@0.3.4': {} + '@jsr/std__bytes@1.0.6': {} '@jsr/std__fmt@1.0.9': {} diff --git a/shared/utils/emoji.ts b/shared/utils/emoji.ts index 09d7f9764..b61730922 100644 --- a/shared/utils/emoji.ts +++ b/shared/utils/emoji.ts @@ -1907,7 +1907,7 @@ const emojis = { const emojisKeysRegex = new RegExp( Object.keys(emojis) - .map(key => `:${RegExp.escape(key)}:`) + .map(key => `:${key}:`) .join('|'), 'g', ) From 77ce5a74261527dd40ca0f6cb87726e164174d25 Mon Sep 17 00:00:00 2001 From: "Willow (GHOST)" Date: Fri, 13 Feb 2026 15:17:07 +0000 Subject: [PATCH 3/3] chore: escape more --- shared/utils/dev-dependency.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/shared/utils/dev-dependency.ts b/shared/utils/dev-dependency.ts index f45449ba5..ae30d8855 100644 --- a/shared/utils/dev-dependency.ts +++ b/shared/utils/dev-dependency.ts @@ -1,3 +1,5 @@ +import { regExpEscape } from '@li/regexp-escape-polyfill' + export type DevDependencySuggestionReason = 'known-package' | 'readme-hint' export interface DevDependencySuggestion { @@ -59,15 +61,11 @@ function isKnownDevDependencyPackage(packageName: string): boolean { ) } -function escapeRegExp(text: string): string { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -} - function hasReadmeDevInstallHint(packageName: string, readmeContent?: string | null): boolean { if (!readmeContent) return false - const escapedName = escapeRegExp(packageName) - const escapedNpmName = escapeRegExp(`npm:${packageName}`) + const escapedName = regExpEscape(packageName) + const escapedNpmName = regExpEscape(`npm:${packageName}`) const packageSpec = `(?:${escapedName}|${escapedNpmName})(?:@[\\w.-]+)?` const patterns = [