From 598b5e690c0ec9768203fe442c01dbc7b6100eff Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:52:08 +0800 Subject: [PATCH] feat(updater): sanitize release notes and standardize semver --- package.json | 3 + pnpm-lock.yaml | 224 ++++++++++++++++++ src/components/setting/mods/update-viewer.tsx | 51 +++- src/services/update.ts | 67 +----- 4 files changed, 281 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index 5733db8ec..95ef1d8a8 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@tauri-apps/plugin-updater": "2.10.1", "ahooks": "^3.9.7", "cidr-block": "^2.3.0", + "compare-versions": "^6.1.1", "dayjs": "1.11.21", "foxact": "^0.3.4", "foxts": "^5.4.2", @@ -78,6 +79,8 @@ "react-markdown": "10.1.0", "react-router": "^8.0.0", "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "swr": "^2.4.2", "tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#refactor-use-reqwest-ipc", "types-pac": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90da62b2e..1cca367d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,9 @@ importers: cidr-block: specifier: ^2.3.0 version: 2.3.0 + compare-versions: + specifier: ^6.1.1 + version: 6.1.1 dayjs: specifier: 1.11.21 version: 1.11.21 @@ -220,6 +223,12 @@ importers: rehype-raw: specifier: ^7.0.0 version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 swr: specifier: ^2.4.2 version: 2.5.1(react@19.2.8) @@ -2569,6 +2578,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-import-context@0.1.9: resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2903,6 +2916,9 @@ packages: hast-util-raw@9.1.0: resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -3199,6 +3215,9 @@ packages: magic-string@1.2.3: resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@14.0.0: resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} engines: {node: '>= 18'} @@ -3208,9 +3227,30 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.3: resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -3247,6 +3287,27 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -3595,12 +3656,21 @@ packages: rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -6290,6 +6360,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: get-tsconfig: 4.14.3 @@ -6729,6 +6801,12 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.5 + '@ungap/structured-clone': 1.3.4 + unist-util-position: 5.0.0 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 @@ -7011,10 +7089,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + marked@14.0.0: {} math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 @@ -7032,6 +7119,63 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -7138,6 +7282,64 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -7569,6 +7771,22 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-sanitize: 5.0.2 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -7586,6 +7804,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index 569f53d6f..e80ac9d93 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -58,16 +58,50 @@ const GITHUB_ALERT_PATTERN = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][\t ]*\n?/i const GITHUB_ALERT_CLASS_PATTERN = /markdown-alert-(note|tip|important|warning|caution)/ +const UPDATE_MARKDOWN_ID_PREFIX = 'update-markdown-' const shouldShowReleaseNotes = (language: string) => language === 'zh' const LazyReactMarkdown = lazy(async () => { - const [{ default: ReactMarkdown }, { default: rehypeRaw }] = - await Promise.all([import('react-markdown'), import('rehype-raw')]) + const [ + { default: ReactMarkdown }, + { default: rehypeRaw }, + { default: rehypeSanitize, defaultSchema }, + { default: remarkGfm }, + ] = await Promise.all([ + import('react-markdown'), + import('rehype-raw'), + import('rehype-sanitize'), + import('remark-gfm'), + ]) + + const sanitizeSchema = { + ...defaultSchema, + clobberPrefix: UPDATE_MARKDOWN_ID_PREFIX, + attributes: { + ...defaultSchema.attributes, + blockquote: [ + ...(defaultSchema.attributes?.blockquote ?? []), + [ + 'className', + 'markdown-alert', + ...Object.keys(GITHUB_ALERTS).map((type) => `markdown-alert-${type}`), + ], + ], + p: [ + ...(defaultSchema.attributes?.p ?? []), + ['className', 'markdown-alert-title'], + ], + }, + } return { default: (props: ReactMarkdownOptions) => ( - + ), } }) @@ -425,18 +459,27 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { remarkPlugins={[remarkGitHubAlertsPlugin]} components={{ a: ({ href, children, ...props }) => { + const isFragment = href?.startsWith('#') ?? false + const renderedHref = isFragment + ? `#${UPDATE_MARKDOWN_ID_PREFIX}${href?.slice(1)}` + : href + ? '#' + : undefined + return ( { + if (isFragment) return event.preventDefault() if (!href) return openUrlWithNotice(href) }} onAuxClick={(event) => { + if (isFragment) return event.preventDefault() if (event.button === 1 && href) { openUrlWithNotice(href) diff --git a/src/services/update.ts b/src/services/update.ts index a35856c86..dccf1acf2 100644 --- a/src/services/update.ts +++ b/src/services/update.ts @@ -3,14 +3,10 @@ import { type CheckOptions, type Update, } from '@tauri-apps/plugin-updater' +import { compareVersions as compareSemver } from 'compare-versions' import { version as appVersion } from '@root/package.json' -type VersionParts = { - main: number[] - pre: (number | string)[] -} - const SEMVER_FULL_REGEX = /^\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/ const SEMVER_SEARCH_REGEX = @@ -36,62 +32,13 @@ const extractSemver = (input: string | null | undefined): string | null => { return normalizeVersion(match[0]) } -const splitVersion = (version: string | null): VersionParts | null => { - if (!version) return null - const [mainPart, preRelease] = version.split('-') - const main = mainPart - .split('.') - .map((part) => Number.parseInt(part, 10)) - .map((num) => (Number.isNaN(num) ? 0 : num)) - - const pre = - preRelease?.split('.').map((token) => { - const numeric = Number.parseInt(token, 10) - return Number.isNaN(numeric) ? token : numeric - }) ?? [] - - return { main, pre } -} - -const compareVersionParts = (a: VersionParts, b: VersionParts): number => { - const length = Math.max(a.main.length, b.main.length) - for (let i = 0; i < length; i += 1) { - const diff = (a.main[i] ?? 0) - (b.main[i] ?? 0) - if (diff !== 0) return diff > 0 ? 1 : -1 - } - - if (a.pre.length === 0 && b.pre.length === 0) return 0 - if (a.pre.length === 0) return 1 - if (b.pre.length === 0) return -1 - - const preLen = Math.max(a.pre.length, b.pre.length) - for (let i = 0; i < preLen; i += 1) { - const aToken = a.pre[i] - const bToken = b.pre[i] - if (aToken === undefined) return -1 - if (bToken === undefined) return 1 - - if (typeof aToken === 'number' && typeof bToken === 'number') { - if (aToken > bToken) return 1 - if (aToken < bToken) return -1 - continue - } - - if (typeof aToken === 'number') return -1 - if (typeof bToken === 'number') return 1 - - if (aToken > bToken) return 1 - if (aToken < bToken) return -1 - } - - return 0 -} - const compareVersions = (a: string | null, b: string | null): number | null => { - const partsA = splitVersion(a) - const partsB = splitVersion(b) - if (!partsA || !partsB) return null - return compareVersionParts(partsA, partsB) + if (!a || !b) return null + try { + return compareSemver(a, b) + } catch { + return null + } } const resolveRemoteVersion = (update: Update): string | null => {