/** * 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```) html = html.replace(/```([\s\S]*?)```/g, '
$1
'); // Inline code (`code`) html = html.replace(/`([^`]+)`/g, '$1'); // 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; }); // 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, '

    '); // Wrap in paragraph if not already wrapped if (!html.startsWith('<')) { 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;