utils.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { marked } from 'marked';
  2. import katex from 'katex';
  3. const renderLatexInHtml = (html: string): string => {
  4. // 匹配块级公式($$...$$)
  5. html = html.replace(/\$\$(.*?)\$\$/gs, (_match, formula) => {
  6. return katex.renderToString(formula.trim(), {
  7. displayMode: true,
  8. throwOnError: false,
  9. strict: false,
  10. trust: true,
  11. });
  12. });
  13. // 匹配行内公式($...$)
  14. html = html.replace(/\$(.*?)\$/g, (_match, formula) => {
  15. return katex.renderToString(formula.trim(), {
  16. displayMode: false,
  17. throwOnError: false,
  18. strict: false,
  19. });
  20. });
  21. return html;
  22. };
  23. export const renderMarkdown = (text: string, icons?: { copy: string; download: string; preview: string; copySuccess: string }): string => {
  24. if (!text) return '';
  25. const processed = text.replace(/<br\s*\/?>/gi, ' \n');
  26. let html = marked.parse(processed) as string;
  27. html = renderLatexInHtml(html);
  28. return icons ? wrapTables(html, icons) : html;
  29. };
  30. export const wrapTables = (html: string, icons: { copy: string; download: string; preview: string; copySuccess: string }): string => {
  31. return html.replace(/<table>([\s\S]*?)<\/table>/g, (match) => {
  32. const encodedTable = encodeURIComponent(match);
  33. return `<div class="markdown-table-wrapper">
  34. <div class="table-actions">
  35. <span class="table-action-btn" data-action="copy" data-table="${encodedTable}" data-icon="${icons.copy}" data-icon-success="${icons.copySuccess}" title="复制Markdown">
  36. <img src="${icons.copy}" width="14" height="14" />
  37. </span>
  38. <span class="table-action-btn" data-action="csv" data-table="${encodedTable}" title="下载CSV">
  39. <img src="${icons.download}" width="14" height="14" />
  40. </span>
  41. <span class="table-action-btn" data-action="preview" data-table="${encodedTable}" title="预览表格">
  42. <img src="${icons.preview}" width="14" height="14" />
  43. </span>
  44. </div>
  45. ${match}
  46. </div>`;
  47. });
  48. };
  49. export const tableHtmlToMarkdown = (tableHtml: string): string => {
  50. const parser = new DOMParser();
  51. const doc = parser.parseFromString(tableHtml, 'text/html');
  52. const table = doc.querySelector('table');
  53. if (!table) return tableHtml;
  54. const rows: string[][] = [];
  55. table.querySelectorAll('tr').forEach((tr) => {
  56. const cells: string[] = [];
  57. tr.querySelectorAll('th, td').forEach((cell) => {
  58. cells.push(cell.textContent?.trim() || '');
  59. });
  60. rows.push(cells);
  61. });
  62. if (rows.length === 0) return tableHtml;
  63. const colCount = Math.max(...rows.map((r) => r.length));
  64. const normalized = rows.map((r) => {
  65. while (r.length < colCount) r.push('');
  66. return r;
  67. });
  68. const lines: string[] = [];
  69. lines.push('| ' + normalized[0].join(' | ') + ' |');
  70. lines.push('| ' + normalized[0].map(() => '---').join(' | ') + ' |');
  71. for (let i = 1; i < normalized.length; i++) {
  72. lines.push('| ' + normalized[i].join(' | ') + ' |');
  73. }
  74. return lines.join('\n');
  75. };
  76. export const tableHtmlToCsv = (tableHtml: string): string => {
  77. const parser = new DOMParser();
  78. const doc = parser.parseFromString(tableHtml, 'text/html');
  79. const table = doc.querySelector('table');
  80. if (!table) return '';
  81. const rows: string[] = [];
  82. table.querySelectorAll('tr').forEach((tr) => {
  83. const cells: string[] = [];
  84. tr.querySelectorAll('th, td').forEach((cell) => {
  85. const text = (cell.textContent?.trim() || '').replace(/"/g, '""');
  86. cells.push(`"${text}"`);
  87. });
  88. rows.push(cells.join(','));
  89. });
  90. return '' + rows.join('\n');
  91. };
  92. export const formatFileSize = (bytes: number): string => {
  93. if (bytes < 1024) return bytes + ' B';
  94. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
  95. return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  96. };
  97. export const isImageFile = (filename: string): boolean => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif)$/i.test(filename);
  98. export const isPdfFile = (filename: string): boolean => /\.pdf$/i.test(filename);
  99. export const isVideoFile = (filename: string): boolean => /\.(mp4|webm|ogg|mov|avi|mkv|flv|wmv|m4v)$/i.test(filename);
  100. export const isAudioFile = (filename: string): boolean => /\.(mp3|wav|ogg|aac|flac|wma|m4a|opus)$/i.test(filename);
  101. export const isTextFile = (filename: string): boolean =>
  102. /\.(txt|md|json|csv|xml|yaml|yml|log|ini|toml|cfg|conf|env|gitignore|dockerignore|editorconfig|prettierrc|eslintrc|babelrc|properties|sh|bat|ps1|cmd|bash|zsh|fish|sql|graphql|proto|js|ts|jsx|tsx|vue|css|scss|sass|less|html|htm|svelte|py|java|kt|kts|c|cpp|h|hpp|cc|cxx|cs|go|rs|rb|php|swift|m|mm|r|R|lua|pl|pm|hs|ex|exs|erl|clj|scala|dart|zig|nim|v|sol|tf|hcl|gradle|cmake|makefile|mk)$/i.test(
  103. filename
  104. );
  105. export const getFileExtension = (filename: string): string => {
  106. const match = filename.match(/\.([^.]+)$/);
  107. return match ? `.${match[1]}`.toUpperCase() : '';
  108. };