app_markdown.js 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. function escapeHtml(text) {
  2. return String(text || "")
  3. .replace(/&/g, "&")
  4. .replace(/</g, "&lt;")
  5. .replace(/>/g, "&gt;")
  6. .replace(/"/g, "&quot;")
  7. .replace(/'/g, "&#39;");
  8. }
  9. function sanitizeMarkdownUrl(url) {
  10. const s = String(url || "").trim();
  11. if (!s) return "";
  12. if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s)) {
  13. const scheme = s.split(":", 1)[0].toLowerCase();
  14. if (!["http", "https", "mailto"].includes(scheme)) return "";
  15. }
  16. return s;
  17. }
  18. function renderMarkdown(md) {
  19. const raw = String(md || "");
  20. const escaped = escapeHtml(raw);
  21. const blocks = [];
  22. const placeholder = (i) => `@@BLOCK_${i}@@`;
  23. const fenced = escaped.replace(/```([\s\S]*?)```/g, (_m, code) => {
  24. const html = `<pre class="code"><code>${code.replace(/^\n+|\n+$/g, "")}</code></pre>`;
  25. blocks.push(html);
  26. return placeholder(blocks.length - 1);
  27. });
  28. let html = fenced
  29. .replace(/^### (.*)$/gm, "<h3>$1</h3>")
  30. .replace(/^## (.*)$/gm, "<h2>$1</h2>")
  31. .replace(/^# (.*)$/gm, "<h1>$1</h1>")
  32. .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_m, alt, url) => {
  33. const safeUrl = sanitizeMarkdownUrl(url);
  34. if (!safeUrl) return `<span class="muted">[图片已拦截]</span>`;
  35. return `<img class="md-img" alt="${alt}" src="${escapeHtml(safeUrl)}" />`;
  36. })
  37. .replace(/@\[(video)\]\(([^)]+)\)/g, (_m, _t, url) => {
  38. const safeUrl = sanitizeMarkdownUrl(url);
  39. if (!safeUrl) return `<span class="muted">[视频已拦截]</span>`;
  40. return `<video class="md-video" controls src="${escapeHtml(safeUrl)}"></video>`;
  41. })
  42. .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) => {
  43. const safeUrl = sanitizeMarkdownUrl(url);
  44. if (!safeUrl) return `<span>${text}</span>`;
  45. return `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener">${text}</a>`;
  46. })
  47. .replace(/`([^`]+)`/g, "<code>$1</code>")
  48. .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
  49. .replace(/\*([^*\n]+)\*/g, "<em>$1</em>");
  50. html = html.replace(/\n{2,}/g, "\n\n");
  51. html = html
  52. .split("\n\n")
  53. .map((p) => {
  54. if (p.startsWith("@@BLOCK_")) return p;
  55. if (/^<h[1-3]>/.test(p.trim()) || /^<pre /.test(p.trim())) return p;
  56. const lines = p.split("\n").join("<br>");
  57. return `<p>${lines}</p>`;
  58. })
  59. .join("\n");
  60. blocks.forEach((b, i) => {
  61. html = html.replaceAll(placeholder(i), b);
  62. });
  63. return html;
  64. }