Private
Public Access
1
0
Files
u-desk/frontend/src/utils/markedExtensions.ts
绝尘 6bee55b96f 新增:SFTP直连+连接池+autoConnect+文件服务器端口自动回退
- SFTP模块:连接/断开/文件CRUD/系统信息采集/base64二进制写入
- 连接池:多服务器同时在线,瞬间切换profile
- autoConnect:启动时自动连接所有非本地服务器
- 端口自动回退:listenWithFallback消除TOCTOU,解决端口冲突崩溃
- 文件服务器URL集中管理:file-server.ts消除8+处硬编码端口
- Sidebar设置面板:添加服务器/自动连接/自动刷新开关
- 修复:validateFilePath越界panic、正则预编译
- 修复:注释准确性(RemoveAll/端口8073/动态端口文档)
2026-05-04 15:33:19 +08:00

252 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { marked } from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/lib/common'
// 按需导入 common 包不包含的语言
import 'highlight.js/lib/languages/powershell'
import 'highlight.js/lib/languages/dos'
import 'highlight.js/lib/languages/autohotkey'
import 'highlight.js/lib/languages/latex'
import 'highlight.js/lib/languages/dockerfile'
import 'highlight.js/lib/languages/cmake'
import 'highlight.js/lib/languages/scala'
import 'highlight.js/lib/languages/dart'
import { getHljsLanguage } from './languageMap'
let mermaidInstance: typeof import('mermaid').default | null = null
let mermaidTheme: string | null = null
// 检测当前是否为暗色主题
function isDarkTheme(): boolean {
if (typeof document === 'undefined') return false
return document.body.getAttribute('arco-theme')?.includes('dark') ?? false
}
async function loadMermaid() {
const currentTheme = isDarkTheme() ? 'dark' : 'default'
if (mermaidInstance && mermaidTheme === currentTheme) {
return mermaidInstance
}
if (!mermaidInstance) {
const m = await import('mermaid')
mermaidInstance = m.default
}
mermaidInstance.initialize({
startOnLoad: false,
theme: currentTheme,
securityLevel: 'strict',
themeVariables: Object.assign({
primaryColor: '#165DFF',
primaryTextColor: '#ffffff',
primaryBorderColor: '#4080FF'
}, currentTheme === 'dark' ? {
lineColor: '#4E5969',
secondaryColor: '#0E42D2',
tertiaryColor: '#0FC6C2',
mainBkg: '#17171A',
nodeBorder: '#165DFF',
clusterBkg: '#232324',
titleColor: '#FFFFFF',
edgeLabelBackground: '#232324'
} : {
lineColor: '#86909C',
secondaryColor: '#E8F3FF',
tertiaryColor: '#722ED1',
mainBkg: '#F2F3F5',
nodeBorder: '#165DFF',
clusterBkg: '#F7F8FA',
titleColor: '#1D2129',
edgeLabelBackground: '#F2F3F5'
})
})
mermaidTheme = currentTheme
return mermaidInstance
}
const renderer = new marked.Renderer()
renderer.code = function(token: any) {
if (token.lang === 'mermaid') {
return `<pre class="mermaid">${token.text}</pre>`
}
const lang = getHljsLanguage(token.lang)
let highlighted: string
try {
highlighted = hljs.highlight(token.text, { language: lang }).value
} catch {
highlighted = token.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
return `<pre><code class="hljs language-${lang}" data-theme="auto">${highlighted}</code></pre>`
}
renderer.heading = function(token: any) {
const raw = token.raw || ''
const depth = token.depth || 1
const text = token.text || ''
const id = raw
.toLowerCase()
.replace(/[^\u4e00-\u9fa5a-z0-9\s-]/g, '')
.trim()
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '') || `heading-${Math.random().toString(36).slice(2, 11)}`
return `<h${depth} id="${id}" class="heading">
${text}<a href="#${id}" class="heading-anchor" aria-hidden="true" title="跳转到此标题">#</a>
</h${depth}>`
}
// ========== 图片相对路径转换支持 ==========
// 当前 Markdown 文件所在目录(由调用方在渲染前设置)
let _currentFileDir: string = ''
// 文件服务器 Base URL由调用方在渲染前设置
let _fileServerBase: string = 'http://localhost:2652/localfs'
/**
* 设置当前 Markdown 文件所在目录(用于图片相对路径→文件服务器 URL 转换)
* @param dir 文件所在目录的绝对路径,如 "D:/docs" 或 "/"(根目录)
*/
export function setCurrentFileDir(dir: string): void {
_currentFileDir = dir
}
/** 获取当前设置的文件目录 */
export function getCurrentFileDir(): string {
return _currentFileDir
}
/**
* 设置文件服务器 Base URL用于图片相对路径转换
* @param base 完整的 base URL 前缀,如 "http://localhost:2652/localfs" 或 "https://host:port/api/v1/proxy/localfs"
*/
export function setFileServerBase(base: string): void {
_fileServerBase = base
}
/**
* 将相对路径图片 src 解析为文件服务器 URL
* - 绝对路径Windows: D:/...、Unix: /usr/...、网络URL、data URI → 不转换
* - 相对路径 → 基于当前文件目录解析为绝对路径,再编码为文件服务器 URL
*/
function resolveImageUrl(src: string, fileServerBase: string): string {
if (!src) return src
// 不转换绝对路径Windows 盘符、网络协议、锚点、data URI
if (/^(?:[a-zA-Z]:[/\\]|\/(?:[^/]|$)|https?:|ftp:|data:|#)/i.test(src)) return src
// 解析相对路径(处理 ../ 和 ./
const dir = _currentFileDir || '/'
const sep = dir.includes('\\') ? '\\' : '/'
let resolved = normalizeRelativePath(dir, src, sep)
// 编码路径(保留 / 分隔符)
const encoded = encodeURIComponent(resolved).replace(/%2F/gi, '/').replace(/%5C/gi, '\\')
return `${fileServerBase}/${encoded}`
}
/**
* 规范化相对路径,处理 .. 和 . 段
*/
function normalizeRelativePath(base: string, relative: string, sep: string): string {
// 确保基础路径不以分隔符结尾
let baseNormalized = base.replace(/[\\/]+$/, '')
if (!baseNormalized) baseNormalized = sep === '/' ? '/' : 'C:\\'
const baseParts = baseNormalized.split(sep).filter(Boolean)
const relParts = relative.split(/[\\/]/).filter(Boolean)
for (const part of relParts) {
if (part === '..') {
baseParts.pop() // 向上一级
} else if (part !== '.') {
baseParts.push(part)
}
}
// 重建路径Windows 绝对路径保留盘符前缀
if (/^[a-zA-Z]:$/i.test(baseNormalized.split(sep)[0] || '')) {
return baseParts.join(sep)
}
// Unix 风格:以 / 开头
return sep + baseParts.join(sep)
}
// 判断是否为本地文件链接(相对路径或本地绝对路径)
const isLocalFileLink = (href: string): boolean => {
if (!href) return false
if (/^(https?|ftp|mailto|tel|data):/i.test(href)) return false
if (href.startsWith('#')) return false
return true
}
// 自定义图片渲染器 - 转换相对路径为文件服务器 URL
renderer.image = function(token: any) {
const src = token.href || ''
const title = token.title || ''
const alt = token.text || ''
const titleAttr = title ? ` title="${title}"` : ''
// 判断是否需要转换(仅处理相对路径,且当前目录已设置)
if (_currentFileDir && !/^(?:[a-zA-Z]:[/\\]|\/(?:[^/]|$)|https?:|ftp:|data:|#)/i.test(src)) {
const resolvedSrc = resolveImageUrl(src, _fileServerBase)
return `<img src="${resolvedSrc}" alt="${alt}"${titleAttr}>`
}
// 默认渲染(绝对路径 / 网络 URL / data URI / 未设置目录时原样输出)
return `<img src="${src}" alt="${alt}"${titleAttr}>`
}
// 自定义链接渲染器 - 支持本地文件链接
renderer.link = function(token: any) {
const href = token.href || ''
const text = this.parser.parseInline(token.tokens) || token.text || ''
const title = token.title || ''
const titleAttr = title ? ` title="${title}"` : ''
if (href.startsWith('#')) {
return `<a href="${href}${titleAttr}">${text}</a>`
}
if (isLocalFileLink(href)) {
return `<a href="javascript:void(0)" data-local-link="${href}" class="local-file-link"${titleAttr}>${text}</a>`
}
return `<a href="${href}" target="_blank" rel="noopener noreferrer"${titleAttr}>${text}</a>`
}
marked.use({ renderer, breaks: true, gfm: true, async: false })
export { marked }
export async function renderMermaidDiagrams() {
const mermaid = await loadMermaid()
if (mermaid) {
// 渲染前保存原始源码textContent 在 SVG 渲染后会变成 CSS 垃圾)
document.querySelectorAll('.mermaid:not([data-mermaid-src])').forEach(pre => {
;(pre as HTMLElement).setAttribute('data-mermaid-src', pre.textContent || '')
})
await mermaid.run()
}
}
/** 清除已渲染内容并重新渲染(用于主题切换后刷新) */
export async function rerenderMermaidDiagrams(container?: HTMLElement | null) {
// 强制重新加载(清除缓存,让下次 loadMermaid 重新初始化新主题)
mermaidInstance = null
mermaidTheme = null
const target = container || document
target.querySelectorAll('.mermaid').forEach(pre => {
const el = pre as HTMLElement
if (el.getAttribute('data-processed')) {
// 从保存的原始源码恢复,而非 textContentSVG 的 textContent 是 CSS 垃圾)
el.innerHTML = el.getAttribute('data-mermaid-src') || ''
el.removeAttribute('data-processed')
}
})
await renderMermaidDiagrams()
}