/** * 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); // 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'); // 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, ftp) html = html.replace(/(?])(\bhttps?:\/\/[^\s<>\[\]()]+)/g, '$1'); // Headers (# H1, ## H2, etc.) html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // Lists // Unordered lists (- item or * item) html = html.replace(/^\s*[-*]\s+(.+)$/gm, '
  • $1
  • '); html = html.replace(/(
  • .*<\/li>)/s, ''); // Ordered lists (1. item) html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '
  • $1
  • '); // Blockquotes (> text) html = html.replace(/^>\s+(.+)$/gm, '
    $1
    '); // Horizontal rules (--- or ***) html = html.replace(/^(?:---|___|\*\*\*)$/gm, '
    '); // Line breaks (two spaces at end of line or double newline) html = html.replace(/ \n/g, '
    '); html = html.replace(/\n\n/g, '

    '); // Restore code blocks and inline code codeBlocks.forEach((block, i) => { html = html.replace('%%CODEBLOCK' + i + '%%', block); }); inlineCodes.forEach((code, i) => { html = html.replace('%%INLINECODE' + i + '%%', code); }); // Wrap in paragraph if not already wrapped if (!html.startsWith('<')) { html = '

    ' + html + '

    '; } return html; } /** * Parse markdown tables * Supports: | Header | Header | * |--------|--------| * | Cell | Cell | */ function parseMarkdownTables(html) { const lines = html.split('\n'); const result = []; let inTable = false; let tableRows = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Check if line is a table row (starts and ends with |, or has | in the middle) if (line.match(/^\|.*\|$/) || line.match(/^[^|]+\|[^|]+/)) { // Check if next line is separator (|---|---|) const nextLine = lines[i + 1] ? lines[i + 1].trim() : ''; const isSeparator = line.match(/^\|?[\s-:|]+\|[\s-:|]+\|?$/); if (!inTable && !isSeparator) { // Start of table - check if this is a header row if (nextLine.match(/^\|?[\s-:|]+\|[\s-:|]+\|?$/)) { inTable = true; tableRows.push({ type: 'header', content: line }); continue; } } if (inTable) { if (isSeparator) { // Skip separator line continue; } tableRows.push({ type: 'body', content: line }); continue; } } // Not a table row - flush any accumulated table if (inTable && tableRows.length > 0) { result.push(buildTable(tableRows)); tableRows = []; inTable = false; } result.push(lines[i]); } // Flush remaining table if (tableRows.length > 0) { result.push(buildTable(tableRows)); } return result.join('\n'); } /** * Build HTML table from parsed rows */ function buildTable(rows) { if (rows.length === 0) return ''; let html = ''; rows.forEach((row, index) => { const cells = row.content.split('|').filter(cell => cell.trim() !== ''); const tag = row.type === 'header' ? 'th' : 'td'; const wrapper = row.type === 'header' ? 'thead' : (index === 1 ? 'tbody' : ''); if (wrapper === 'thead') html += ''; if (wrapper === 'tbody') html += ''; html += ''; cells.forEach(cell => { html += `<${tag}>${cell.trim()}`; }); html += ''; if (row.type === 'header') html += ''; }); html += '
    '; return html; } // Apply markdown rendering to all elements with data-markdown attribute function renderMarkdownElements() { document.querySelectorAll('[data-markdown]').forEach(element => { const markdownText = element.getAttribute('data-markdown') || element.textContent; element.innerHTML = parseMarkdown(markdownText); }); } // Apply markdown to description and comments on page load document.addEventListener('DOMContentLoaded', renderMarkdownElements); // Expose for manual use window.parseMarkdown = parseMarkdown; window.renderMarkdownElements = renderMarkdownElements; // ======================================== // Rich Text Editor Toolbar Functions // ======================================== /** * Insert markdown formatting around selection */ function insertMarkdownFormat(textareaId, prefix, suffix) { const textarea = document.getElementById(textareaId); if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; const selectedText = text.substring(start, end); // Insert formatting const newText = text.substring(0, start) + prefix + selectedText + suffix + text.substring(end); textarea.value = newText; // Set cursor position if (selectedText) { textarea.setSelectionRange(start + prefix.length, end + prefix.length); } else { textarea.setSelectionRange(start + prefix.length, start + prefix.length); } textarea.focus(); // Trigger input event to update preview if enabled textarea.dispatchEvent(new Event('input', { bubbles: true })); } /** * Insert markdown at cursor position */ function insertMarkdownText(textareaId, text) { const textarea = document.getElementById(textareaId); if (!textarea) return; const start = textarea.selectionStart; const value = textarea.value; textarea.value = value.substring(0, start) + text + value.substring(start); textarea.setSelectionRange(start + text.length, start + text.length); textarea.focus(); textarea.dispatchEvent(new Event('input', { bubbles: true })); } /** * Toolbar button handlers */ function toolbarBold(textareaId) { insertMarkdownFormat(textareaId, '**', '**'); } function toolbarItalic(textareaId) { insertMarkdownFormat(textareaId, '_', '_'); } function toolbarCode(textareaId) { const textarea = document.getElementById(textareaId); if (!textarea) return; const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); // Use code block for multi-line, inline code for single line if (selectedText.includes('\n')) { insertMarkdownFormat(textareaId, '```\n', '\n```'); } else { insertMarkdownFormat(textareaId, '`', '`'); } } function toolbarLink(textareaId) { const textarea = document.getElementById(textareaId); if (!textarea) return; const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd); if (selectedText) { // Wrap selected text as link text insertMarkdownFormat(textareaId, '[', '](url)'); } else { insertMarkdownText(textareaId, '[link text](url)'); } } function toolbarList(textareaId) { const textarea = document.getElementById(textareaId); if (!textarea) return; const start = textarea.selectionStart; const text = textarea.value; // Find start of current line let lineStart = start; while (lineStart > 0 && text[lineStart - 1] !== '\n') { lineStart--; } // Insert list marker at beginning of line textarea.value = text.substring(0, lineStart) + '- ' + text.substring(lineStart); textarea.setSelectionRange(start + 2, start + 2); textarea.focus(); textarea.dispatchEvent(new Event('input', { bubbles: true })); } function toolbarHeading(textareaId) { const textarea = document.getElementById(textareaId); if (!textarea) return; const start = textarea.selectionStart; const text = textarea.value; // Find start of current line let lineStart = start; while (lineStart > 0 && text[lineStart - 1] !== '\n') { lineStart--; } // Insert heading marker at beginning of line textarea.value = text.substring(0, lineStart) + '## ' + text.substring(lineStart); textarea.setSelectionRange(start + 3, start + 3); textarea.focus(); textarea.dispatchEvent(new Event('input', { bubbles: true })); } function toolbarQuote(textareaId) { const textarea = document.getElementById(textareaId); if (!textarea) return; const start = textarea.selectionStart; const text = textarea.value; // Find start of current line let lineStart = start; while (lineStart > 0 && text[lineStart - 1] !== '\n') { lineStart--; } // Insert quote marker at beginning of line textarea.value = text.substring(0, lineStart) + '> ' + text.substring(lineStart); textarea.setSelectionRange(start + 2, start + 2); textarea.focus(); textarea.dispatchEvent(new Event('input', { bubbles: true })); } /** * Create and insert toolbar HTML for a textarea */ function createEditorToolbar(textareaId, containerId) { const container = document.getElementById(containerId); if (!container) return; const toolbar = document.createElement('div'); toolbar.className = 'editor-toolbar'; toolbar.innerHTML = ` `; container.insertBefore(toolbar, container.firstChild); } // Expose toolbar functions globally window.toolbarBold = toolbarBold; window.toolbarItalic = toolbarItalic; window.toolbarCode = toolbarCode; window.toolbarLink = toolbarLink; window.toolbarList = toolbarList; window.toolbarHeading = toolbarHeading; window.toolbarQuote = toolbarQuote; window.createEditorToolbar = createEditorToolbar; window.insertMarkdownFormat = insertMarkdownFormat; window.insertMarkdownText = insertMarkdownText; // ======================================== // Auto-link URLs in plain text (non-markdown) // ======================================== /** * Convert plain text URLs to clickable links * Used for non-markdown comments */ function autoLinkUrls(text) { if (!text) return ''; // Match URLs that aren't already in an href attribute return text.replace(/(?])(https?:\/\/[^\s<>\[\]()]+)/g, '$1'); } /** * Process all non-markdown comment elements to auto-link URLs */ function processPlainTextComments() { document.querySelectorAll('.comment-text:not([data-markdown])').forEach(element => { // Only process if not already processed if (element.dataset.linksProcessed) return; element.innerHTML = autoLinkUrls(element.innerHTML); element.dataset.linksProcessed = 'true'; }); } // Run on page load document.addEventListener('DOMContentLoaded', function() { processPlainTextComments(); }); // Expose for manual use window.autoLinkUrls = autoLinkUrls; window.processPlainTextComments = processPlainTextComments;