/** * Simple Markdown Parser for Tinker Tickets * Supports basic markdown formatting without external dependencies */ function parseMarkdown(markdown) { if (!markdown) return ''; let html = markdown; // Escape HTML first to prevent XSS html = html.replace(/&/g, '&') .replace(//g, '>'); // Ticket references (#123456789) - convert to clickable links html = html.replace(/#(\d{9})\b/g, '#$1'); // Code blocks (```code```) - preserve content and don't process further const codeBlocks = []; html = html.replace(/```([\s\S]*?)```/g, function(match, code) { codeBlocks.push('
' + code + '
'); return '%%CODEBLOCK' + (codeBlocks.length - 1) + '%%'; }); // Inline code (`code`) - preserve and don't process further const inlineCodes = []; html = html.replace(/`([^`]+)`/g, function(match, code) { inlineCodes.push('' + code + ''); return '%%INLINECODE' + (inlineCodes.length - 1) + '%%'; }); // Tables (must be processed before other block elements) html = parseMarkdownTables(html); // Emoji :name: — common set html = replaceEmoji(html); // Bold (**text** or __text__) html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); // Italic (*text* or _text_) html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/_(.+?)_/g, '$1'); // Strikethrough (~~text~~) — must run before subscript (~) html = html.replace(/~~(.+?)~~/g, '$1'); // Highlight (==text==) html = html.replace(/==(.+?)==/g, '$1'); // Subscript H~2~O — single tilde (not preceded/followed by another tilde) html = html.replace(/(?$1'); // Superscript X^2^ — caret pair html = html.replace(/\^([^\^\n]+?)\^/g, '$1'); // Images ![alt](url) - must come before link handler html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, url) { if (/^https?:/i.test(url)) { return '' + alt + ''; } return match; }); // Links [text](url) - only allow safe protocols html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) { // Only allow http, https, mailto protocols if (/^(https?:|mailto:|\/)/i.test(url)) { return '' + text + ''; } // Block potentially dangerous protocols (javascript:, data:, etc.) return text; }); // Auto-link bare URLs (http, https) html = html.replace(/(?])(\bhttps?:\/\/[^\s<>\[\]()]+)/g, '$1'); // Headings with optional {#id} anchor — ### My Heading {#my-id} html = html.replace(/^(#{1,6})\s+(.+?)\s*(?:\{#([a-z0-9_-]+)\})?$/gm, function(match, hashes, text, id) { const level = hashes.length; const idAttr = id ? ' id="' + id + '"' : ''; return '' + text + ''; }); // Task lists — must run before general list processing html = html.replace(/^\s*-\s+\[x\]\s+(.+)$/gim, '
  • $1
  • '); html = html.replace(/^\s*-\s+\[ \]\s+(.+)$/gm, '
  • $1
  • '); // Unordered lists (- item or * item) — wrap consecutive
  • in