refactor(CodeBlock): support more file extensions for code downloading (#7192)

This commit is contained in:
one 2025-06-19 15:09:01 +08:00 committed by GitHub
parent 26cb37c9be
commit 28b58d8e49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 137 additions and 10 deletions

View File

@ -68,12 +68,16 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['pyodide'] exclude: ['pyodide'],
esbuildOptions: {
target: 'esnext' // for dev
}
}, },
worker: { worker: {
format: 'es' format: 'es'
}, },
build: { build: {
target: 'esnext', // for build
rollupOptions: { rollupOptions: {
input: { input: {
index: resolve(__dirname, 'src/renderer/index.html'), index: resolve(__dirname, 'src/renderer/index.html'),

View File

@ -168,6 +168,7 @@
"husky": "^9.1.7", "husky": "^9.1.7",
"i18next": "^23.11.5", "i18next": "^23.11.5",
"jest-styled-components": "^7.2.0", "jest-styled-components": "^7.2.0",
"linguist-languages": "^8.0.0",
"lint-staged": "^15.5.0", "lint-staged": "^15.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",

View File

@ -4,7 +4,7 @@ import { CodeTool, CodeToolbar, TOOL_SPECS, useCodeTool } from '@renderer/compon
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { pyodideService } from '@renderer/services/PyodideService' import { pyodideService } from '@renderer/services/PyodideService'
import { extractTitle } from '@renderer/utils/formats' import { extractTitle } from '@renderer/utils/formats'
import { isValidPlantUML } from '@renderer/utils/markdown' import { getExtensionByLanguage, isValidPlantUML } from '@renderer/utils/markdown'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { CirclePlay, CodeXml, Copy, Download, Eye, Square, SquarePen, SquareSplitHorizontal } from 'lucide-react' import { CirclePlay, CodeXml, Copy, Download, Eye, Square, SquarePen, SquareSplitHorizontal } from 'lucide-react'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
@ -67,23 +67,21 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' }) window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' })
}, [children, t]) }, [children, t])
const handleDownloadSource = useCallback(() => { const handleDownloadSource = useCallback(async () => {
let fileName = '' let fileName = ''
// 尝试提取标题 // 尝试提取 HTML 标题
if (language === 'html' && children.includes('</html>')) { if (language === 'html' && children.includes('</html>')) {
const title = extractTitle(children) fileName = extractTitle(children) || ''
if (title) {
fileName = `${title}.html`
}
} }
// 默认使用日期格式命名 // 默认使用日期格式命名
if (!fileName) { if (!fileName) {
fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}` fileName = `${dayjs().format('YYYYMMDDHHmm')}`
} }
window.api.file.save(fileName, children) const ext = await getExtensionByLanguage(language)
window.api.file.save(`${fileName}${ext}`, children)
}, [children, language]) }, [children, language])
const handleRunScript = useCallback(() => { const handleRunScript = useCallback(() => {

View File

@ -7,6 +7,7 @@ import {
convertMathFormula, convertMathFormula,
findCitationInChildren, findCitationInChildren,
getCodeBlockId, getCodeBlockId,
getExtensionByLanguage,
markdownToPlainText, markdownToPlainText,
removeTrailingDoubleSpaces, removeTrailingDoubleSpaces,
updateCodeBlock updateCodeBlock
@ -143,6 +144,67 @@ describe('markdown', () => {
}) })
}) })
describe('getExtensionByLanguage', () => {
// 批量测试语言名称到扩展名的映射
const testLanguageExtensions = async (testCases: Record<string, string>) => {
for (const [language, expectedExtension] of Object.entries(testCases)) {
const result = await getExtensionByLanguage(language)
expect(result).toBe(expectedExtension)
}
}
it('should return extension for exact language name match', async () => {
await testLanguageExtensions({
'4D': '.4dm',
'C#': '.cs',
JavaScript: '.js',
TypeScript: '.ts',
'Objective-C++': '.mm',
Python: '.py',
SVG: '.svg',
'Visual Basic .NET': '.vb'
})
})
it('should return extension for case-insensitive language name match', async () => {
await testLanguageExtensions({
'4d': '.4dm',
'c#': '.cs',
javascript: '.js',
typescript: '.ts',
'objective-c++': '.mm',
python: '.py',
svg: '.svg',
'visual basic .net': '.vb'
})
})
it('should return extension for language aliases', async () => {
await testLanguageExtensions({
js: '.js',
node: '.js',
'obj-c++': '.mm',
'objc++': '.mm',
'objectivec++': '.mm',
py: '.py',
'visual basic': '.vb'
})
})
it('should return fallback extension for unknown languages', async () => {
await testLanguageExtensions({
'unknown-language': '.unknown-language',
custom: '.custom'
})
})
it('should handle empty string input', async () => {
await testLanguageExtensions({
'': '.'
})
})
})
describe('getCodeBlockId', () => { describe('getCodeBlockId', () => {
it('should generate ID from position information', () => { it('should generate ID from position information', () => {
// 从位置信息生成ID // 从位置信息生成ID

View File

@ -54,6 +54,60 @@ export function removeTrailingDoubleSpaces(markdown: string): string {
return markdown.replace(/ {2}$/gm, '') return markdown.replace(/ {2}$/gm, '')
} }
const predefinedExtensionMap: Record<string, string> = {
html: '.html',
javascript: '.js',
typescript: '.ts',
python: '.py',
json: '.json',
markdown: '.md',
text: '.txt'
}
/**
*
* -
* -
* @param language
* @returns
*/
export async function getExtensionByLanguage(language: string): Promise<string> {
const lowerLanguage = language.toLowerCase()
// 常用的扩展名
const predefined = predefinedExtensionMap[lowerLanguage]
if (predefined) {
return predefined
}
const languages = await import('linguist-languages')
// 精确匹配语言名称
const directMatch = languages[language as keyof typeof languages] as any
if (directMatch?.extensions?.[0]) {
return directMatch.extensions[0]
}
// 大小写不敏感的语言名称匹配
for (const [langName, data] of Object.entries(languages)) {
const languageData = data as any
if (langName.toLowerCase() === lowerLanguage && languageData.extensions?.[0]) {
return languageData.extensions[0]
}
}
// 通过别名匹配
for (const [, data] of Object.entries(languages)) {
const languageData = data as any
if (languageData.aliases?.includes(lowerLanguage)) {
return languageData.extensions?.[0] || `.${language}`
}
}
// 回退到语言名称
return `.${language}`
}
/** /**
* ID * ID
* @param start * @param start

View File

@ -5688,6 +5688,7 @@ __metadata:
i18next: "npm:^23.11.5" i18next: "npm:^23.11.5"
jest-styled-components: "npm:^7.2.0" jest-styled-components: "npm:^7.2.0"
jsdom: "npm:26.1.0" jsdom: "npm:26.1.0"
linguist-languages: "npm:^8.0.0"
lint-staged: "npm:^15.5.0" lint-staged: "npm:^15.5.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
lru-cache: "npm:^11.1.0" lru-cache: "npm:^11.1.0"
@ -11874,6 +11875,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"linguist-languages@npm:^8.0.0":
version: 8.0.0
resolution: "linguist-languages@npm:8.0.0"
checksum: 10c0/eaae46254247b9aa5b287ac98e062e7fe859314328ce305e34e152bc7bb172d69633999320cb47dc2a710388179712a76bb1ddd6e39e249af2684a4f0a66256c
languageName: node
linkType: hard
"linkify-it@npm:^5.0.0": "linkify-it@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "linkify-it@npm:5.0.0" resolution: "linkify-it@npm:5.0.0"