From cae459ccf31114f7e3229fda01fec1424a31ef58 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sat, 7 Feb 2026 11:35:08 +0800 Subject: [PATCH 1/4] chore: add `eslint-plugin-regexp` --- .oxlintrc.json | 69 ++++++++++++++++++- app/composables/useStructuredFilters.ts | 2 +- app/pages/search.vue | 2 +- app/utils/formatters.ts | 7 +- app/utils/npm/outdated-dependencies.ts | 2 +- cli/src/schemas.ts | 2 +- package.json | 1 + pnpm-lock.yaml | 59 ++++++++++++++++ server/api/registry/org/[org]/packages.get.ts | 2 +- server/api/registry/readme/[...pkg].get.ts | 2 +- server/utils/docs/text.ts | 8 +-- server/utils/npm.ts | 2 +- server/utils/shiki.ts | 2 +- shared/schemas/package.ts | 2 +- shared/utils/git-providers.ts | 2 +- shared/utils/npm.ts | 2 +- shared/utils/parse-basic-frontmatter.ts | 2 +- 17 files changed, 146 insertions(+), 22 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index b0bc5d6ff..3593ccb94 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,7 +1,7 @@ { "$schema": "https://unpkg.com/oxlint/configuration_schema.json", "plugins": ["unicorn", "typescript", "oxc", "vue", "vitest"], - "jsPlugins": ["@e18e/eslint-plugin"], + "jsPlugins": ["@e18e/eslint-plugin", "eslint-plugin-regexp"], "categories": { "correctness": "error", "suspicious": "warn", @@ -17,7 +17,72 @@ "e18e/prefer-timer-args": "error", "e18e/prefer-date-now": "error", "e18e/prefer-regex-test": "error", - "e18e/prefer-array-some": "error" + "e18e/prefer-array-some": "error", + // RegExp - Possible Errors (most critical) + "regexp/no-contradiction-with-assertion": "error", + "regexp/no-dupe-disjunctions": "error", + "regexp/no-empty-alternative": "error", + "regexp/no-empty-capturing-group": "error", + "regexp/no-empty-character-class": "error", + "regexp/no-empty-group": "error", + "regexp/no-empty-lookarounds-assertion": "error", + "regexp/no-escape-backspace": "error", + "regexp/no-invalid-regexp": "error", + "regexp/no-lazy-ends": "error", + "regexp/no-misleading-capturing-group": "error", + "regexp/no-misleading-unicode-character": "error", + "regexp/no-missing-g-flag": "error", + "regexp/no-optional-assertion": "error", + "regexp/no-potentially-useless-backreference": "error", + "regexp/no-super-linear-backtracking": "error", + "regexp/no-useless-assertions": "error", + "regexp/no-useless-backreference": "error", + "regexp/no-useless-dollar-replacements": "error", + "regexp/strict": "error", + // RegExp - Best Practices + "regexp/confusing-quantifier": "warn", + "regexp/control-character-escape": "error", + "regexp/negation": "error", + "regexp/no-dupe-characters-character-class": "error", + "regexp/no-empty-string-literal": "error", + "regexp/no-extra-lookaround-assertions": "error", + "regexp/no-invisible-character": "error", + "regexp/no-legacy-features": "error", + "regexp/no-non-standard-flag": "error", + "regexp/no-obscure-range": "error", + "regexp/no-octal": "error", + "regexp/no-standalone-backslash": "error", + "regexp/no-trivially-nested-assertion": "error", + "regexp/no-trivially-nested-quantifier": "error", + "regexp/no-unused-capturing-group": "warn", + "regexp/no-useless-character-class": "error", + "regexp/no-useless-flag": "error", + "regexp/no-useless-lazy": "error", + "regexp/no-useless-quantifier": "error", + "regexp/no-useless-range": "error", + "regexp/no-useless-set-operand": "error", + "regexp/no-useless-string-literal": "error", + "regexp/no-useless-two-nums-quantifier": "error", + "regexp/no-zero-quantifier": "error", + "regexp/optimal-lookaround-quantifier": "warn", + "regexp/optimal-quantifier-concatenation": "error", + "regexp/prefer-predefined-assertion": "error", + "regexp/prefer-range": "error", + "regexp/prefer-set-operation": "error", + "regexp/simplify-set-operations": "error", + "regexp/use-ignore-case": "error", + // RegExp - Stylistic Issues (less critical, focused on consistency) + "regexp/match-any": "warn", + "regexp/no-useless-escape": "warn", + "regexp/no-useless-non-capturing-group": "warn", + "regexp/prefer-character-class": "warn", + "regexp/prefer-d": "warn", + "regexp/prefer-plus-quantifier": "warn", + "regexp/prefer-question-quantifier": "warn", + "regexp/prefer-star-quantifier": "warn", + "regexp/prefer-unicode-codepoint-escapes": "warn", + "regexp/prefer-w": "warn", + "regexp/sort-flags": "warn" }, "overrides": [ { diff --git a/app/composables/useStructuredFilters.ts b/app/composables/useStructuredFilters.ts index f030739cd..93295e636 100644 --- a/app/composables/useStructuredFilters.ts +++ b/app/composables/useStructuredFilters.ts @@ -42,7 +42,7 @@ export function parseSearchOperators(input: string): ParsedSearchOperators { // Regex to match operators: name:value, desc:value, description:value, kw:value, keyword:value // Value continues until whitespace or next operator - const operatorRegex = /\b(name|desc|description|kw|keyword):([^\s]+)/gi + const operatorRegex = /\b(name|desc|description|kw|keyword):(\S+)/gi let remaining = input let match diff --git a/app/pages/search.vue b/app/pages/search.vue index 85868990b..564ef7e22 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -303,7 +303,7 @@ function isValidNpmName(name: string): boolean { // Must start with alphanumeric if (!/^[a-z0-9]/i.test(name)) return false // Can contain alphanumeric, hyphen, underscore - return /^[a-z0-9_-]+$/i.test(name) + return /^[\w-]+$/.test(name) } /** Validated user/org suggestion */ diff --git a/app/utils/formatters.ts b/app/utils/formatters.ts index e7111c9cb..8d93280f5 100644 --- a/app/utils/formatters.ts +++ b/app/utils/formatters.ts @@ -31,10 +31,9 @@ export function formatCompactNumber( const fmt = (n: number) => { if (decimals <= 0) return Math.round(n).toString() - return n - .toFixed(decimals) - .replace(/\.0+$/, '') - .replace(/(\.\d*?)0+$/, '$1') + const fixed = n.toFixed(decimals) + // Remove trailing zeros after decimal point + return fixed.includes('.') ? fixed.replace(/0+$/, '').replace(/\.$/, '') : fixed } const join = (suffix: string, n: number) => `${sign}${fmt(n)}${space ? ' ' : ''}${suffix}` diff --git a/app/utils/npm/outdated-dependencies.ts b/app/utils/npm/outdated-dependencies.ts index 95b672b77..5e3bbebf7 100644 --- a/app/utils/npm/outdated-dependencies.ts +++ b/app/utils/npm/outdated-dependencies.ts @@ -20,7 +20,7 @@ export interface OutdatedDependencyInfo { */ export function constraintIncludesPrerelease(constraint: string): boolean { return ( - /-(alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || + /-(?:alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || /-\d/.test(constraint) ) } diff --git a/cli/src/schemas.ts b/cli/src/schemas.ts index 95ee4c1ca..6d4fd3883 100644 --- a/cli/src/schemas.ts +++ b/cli/src/schemas.ts @@ -3,7 +3,7 @@ import validateNpmPackageName from 'validate-npm-package-name' // Validation pattern for npm usernames/org names // These follow similar rules: lowercase alphanumeric with hyphens, can't start/end with hyphen -const NPM_USERNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i +const NPM_USERNAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i // ============================================================================ // Base Schemas diff --git a/package.json b/package.json index 9ddbc5614..85d7e7229 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "@vitest/coverage-v8": "4.0.18", "@vue/test-utils": "2.4.6", "axe-core": "4.11.1", + "eslint-plugin-regexp": "3.0.0", "fast-check": "4.5.3", "h3": "1.15.5", "knip": "5.83.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 372568565..70e0c2629 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,6 +240,9 @@ importers: axe-core: specifier: 4.11.1 version: 4.11.1 + eslint-plugin-regexp: + specifier: 3.0.0 + version: 3.0.0(eslint@9.39.2(jiti@2.6.1)) fast-check: specifier: 4.5.3 version: 4.5.3 @@ -5038,6 +5041,10 @@ packages: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} + comment-parser@1.4.5: + resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} + engines: {node: '>= 12.0.0'} + common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -5590,6 +5597,12 @@ packages: eslint-plugin-depend@1.4.0: resolution: {integrity: sha512-MQs+m4nHSfgAO9bJDsBzqw0ofK/AOA0vfeY/6ahofqcUMLeM6/D1sTYs21fOhc17kNU/gn58YCtj20XaAssh2A==} + eslint-plugin-regexp@3.0.0: + resolution: {integrity: sha512-iW7hgAV8NOG6E2dz+VeKpq67YLQ9jaajOKYpoOSic2/q8y9BMdXBKkSR9gcMtbqEhNQzdW41E3wWzvhp8ExYwQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=9.38.0' + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -6587,6 +6600,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdoc-type-pratt-parser@7.1.1: + resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} + engines: {node: '>=20.0.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -8039,6 +8056,10 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -8059,6 +8080,10 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -8271,6 +8296,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -15016,6 +15045,8 @@ snapshots: commander@6.2.1: {} + comment-parser@1.4.5: {} + common-tags@1.8.2: {} commondir@1.0.1: {} @@ -15710,6 +15741,17 @@ snapshots: module-replacements: 2.11.0 semver: 7.7.3 + eslint-plugin-regexp@3.0.0(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + comment-parser: 1.4.5 + eslint: 9.39.2(jiti@2.6.1) + jsdoc-type-pratt-parser: 7.1.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -16949,6 +16991,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsdoc-type-pratt-parser@7.1.1: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -19205,6 +19249,10 @@ snapshots: dependencies: redis-errors: 1.2.0 + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -19232,6 +19280,11 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp-tree@0.1.27: {} regexp.prototype.flags@1.5.4: @@ -19580,6 +19633,12 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scule@1.3.0: {} semver@6.3.1: {} diff --git a/server/api/registry/org/[org]/packages.get.ts b/server/api/registry/org/[org]/packages.get.ts index 113c2068f..28b0dbba8 100644 --- a/server/api/registry/org/[org]/packages.get.ts +++ b/server/api/registry/org/[org]/packages.get.ts @@ -3,7 +3,7 @@ import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants' const NPM_REGISTRY = 'https://registry.npmjs.org' // Validation pattern for npm org names (alphanumeric with hyphens) -const NPM_ORG_NAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i +const NPM_ORG_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i function validateOrgName(name: string): void { if (!name || name.length > 50 || !NPM_ORG_NAME_RE.test(name)) { diff --git a/server/api/registry/readme/[...pkg].get.ts b/server/api/registry/readme/[...pkg].get.ts index e0280133c..ca64a55c0 100644 --- a/server/api/registry/readme/[...pkg].get.ts +++ b/server/api/registry/readme/[...pkg].get.ts @@ -18,7 +18,7 @@ const standardReadmeFilenames = [ ] /** Matches standard README filenames (case-insensitive, for checking registry metadata) */ -const standardReadmePattern = /^readme(\.md|\.markdown)?$/i +const standardReadmePattern = /^readme(?:\.md|\.markdown)?$/i /** * Fetch README from jsdelivr CDN for a specific package version. diff --git a/server/utils/docs/text.ts b/server/utils/docs/text.ts index b152fbfd0..28e0a5362 100644 --- a/server/utils/docs/text.ts +++ b/server/utils/docs/text.ts @@ -54,7 +54,7 @@ export function cleanSymbolName(name: string): string { * Create a URL-safe HTML anchor ID for a symbol. */ export function createSymbolId(kind: string, name: string): string { - return `${kind}-${name}`.replace(/[^a-zA-Z0-9-]/g, '_') + return `${kind}-${name}`.replace(/[^a-z0-9-]/gi, '_') } /** @@ -70,7 +70,7 @@ export function createSymbolId(kind: string, name: string): string { export function parseJsDocLinks(text: string, symbolLookup: SymbolLookup): string { let result = escapeHtml(text) - result = result.replace(/\{@link\s+([^\s}]+)(?:\s+([^}]+))?\}/g, (_, target, label) => { + result = result.replace(/\{@link\s+([^\s}]+)(?:\s+([^\s}]*))?\}/g, (_, target, label) => { const displayText = label || target // External URL @@ -105,7 +105,7 @@ export async function renderMarkdown(text: string, symbolLookup: SymbolLookup): // - \r\n, \n, or \r line endings const codeBlockData: Array<{ lang: string; code: string }> = [] let result = text.replace( - /```[ \t]*(\w*)[ \t]*(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)?```/g, + /```[ \t]*(\w*)(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)?```/g, (_, lang, code) => { const index = codeBlockData.length codeBlockData.push({ lang: lang || 'text', code: code.trim() }) @@ -126,7 +126,7 @@ export async function renderMarkdown(text: string, symbolLookup: SymbolLookup): result = result .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') - .replace(/\n\n+/g, '

') + .replace(/\n{2,}/g, '

') .replace(/\n/g, '
') // Highlight and restore code blocks diff --git a/server/utils/npm.ts b/server/utils/npm.ts index 586762609..7f61feafc 100644 --- a/server/utils/npm.ts +++ b/server/utils/npm.ts @@ -45,7 +45,7 @@ export async function fetchLatestVersionWithFallback(name: string): Promise tag so CSS can control appearance - html = html.replace(/]*)\s+style="[^"]*"/, ']*) style="[^"]*"/, ' in text content (e.g., arrow functions =>) // We need to encode them for HTML validation return escapeRawGt(html) diff --git a/shared/schemas/package.ts b/shared/schemas/package.ts index 355b138d9..7bfde5db3 100644 --- a/shared/schemas/package.ts +++ b/shared/schemas/package.ts @@ -22,7 +22,7 @@ export const PackageNameSchema = v.pipe( export const VersionSchema = v.pipe( v.string(), v.nonEmpty('Version is required'), - v.regex(/^[a-z0-9._+-]+$/i, 'Invalid version format'), + v.regex(/^[\w.+-]+$/, 'Invalid version format'), ) /** diff --git a/shared/utils/git-providers.ts b/shared/utils/git-providers.ts index 0f9760861..e14e82f5f 100644 --- a/shared/utils/git-providers.ts +++ b/shared/utils/git-providers.ts @@ -299,7 +299,7 @@ export function normalizeGitUrl(input: string): string | null { const normalized = raw.replace(/^git\+/, '') // Handle ssh:// and git:// URLs by converting to https:// - if (/^(ssh|git):\/\//i.test(normalized)) { + if (/^(?:ssh|git):\/\//i.test(normalized)) { try { const url = new URL(normalized) const path = url.pathname.replace(/^\/*/, '') diff --git a/shared/utils/npm.ts b/shared/utils/npm.ts index ae918a6c4..54fb00730 100644 --- a/shared/utils/npm.ts +++ b/shared/utils/npm.ts @@ -2,7 +2,7 @@ import { getLatestVersion } from 'fast-npm-meta' import { createError } from 'h3' import validatePackageName from 'validate-npm-package-name' -const NPM_USERNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i +const NPM_USERNAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i const NPM_USERNAME_MAX_LENGTH = 50 /** diff --git a/shared/utils/parse-basic-frontmatter.ts b/shared/utils/parse-basic-frontmatter.ts index 21bba954c..a2c31187f 100644 --- a/shared/utils/parse-basic-frontmatter.ts +++ b/shared/utils/parse-basic-frontmatter.ts @@ -1,5 +1,5 @@ export function parseBasicFrontmatter(fileContent: string): Record { - const match = fileContent.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/) + const match = fileContent.match(/^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/) if (!match?.[1]) return {} return match[1].split('\n').reduce>((acc, line) => { From 45aa48c98d4e8a4c99907af70136ec9a1f853864 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sat, 7 Feb 2026 11:40:57 +0800 Subject: [PATCH 2/4] chore: knip update --- knip.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/knip.ts b/knip.ts index d066b592a..6b7901476 100644 --- a/knip.ts +++ b/knip.ts @@ -42,6 +42,7 @@ const config: KnipConfig = { /** Oxlint plugins don't get picked up yet */ '@e18e/eslint-plugin', + 'eslint-plugin-regexp', ], ignoreUnresolved: ['#components', '#oauth/config'], }, From aaf487242ebee9085f30fd3ab28234a54d381dbe Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sat, 7 Feb 2026 12:03:43 +0800 Subject: [PATCH 3/4] chore: update --- server/utils/docs/text.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/utils/docs/text.ts b/server/utils/docs/text.ts index 28e0a5362..f168399cb 100644 --- a/server/utils/docs/text.ts +++ b/server/utils/docs/text.ts @@ -1,3 +1,4 @@ +/* oxlint-disable regexp/no-super-linear-backtracking */ /** * Text Processing Utilities * @@ -69,8 +70,7 @@ export function createSymbolId(kind: string, name: string): string { */ export function parseJsDocLinks(text: string, symbolLookup: SymbolLookup): string { let result = escapeHtml(text) - - result = result.replace(/\{@link\s+([^\s}]+)(?:\s+([^\s}]*))?\}/g, (_, target, label) => { + result = result.replace(/\{@link\s+([^\s}]+)(?:\s+([^}]+))?\}/g, (_, target, label) => { const displayText = label || target // External URL @@ -105,7 +105,7 @@ export async function renderMarkdown(text: string, symbolLookup: SymbolLookup): // - \r\n, \n, or \r line endings const codeBlockData: Array<{ lang: string; code: string }> = [] let result = text.replace( - /```[ \t]*(\w*)(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)?```/g, + /```[ \t]*(\w*)[ \t]*(?:\r\n|\r|\n)([\s\S]*?)(?:\r\n|\r|\n)?```/g, (_, lang, code) => { const index = codeBlockData.length codeBlockData.push({ lang: lang || 'text', code: code.trim() }) From 137aa981ba42f4d678a5bd511d1b78ef901afcfd Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sat, 7 Feb 2026 12:07:47 +0800 Subject: [PATCH 4/4] chore: revert unnecessary change --- server/utils/docs/text.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/docs/text.ts b/server/utils/docs/text.ts index f168399cb..7cb574903 100644 --- a/server/utils/docs/text.ts +++ b/server/utils/docs/text.ts @@ -70,6 +70,7 @@ export function createSymbolId(kind: string, name: string): string { */ export function parseJsDocLinks(text: string, symbolLookup: SymbolLookup): string { let result = escapeHtml(text) + result = result.replace(/\{@link\s+([^\s}]+)(?:\s+([^}]+))?\}/g, (_, target, label) => { const displayText = label || target