import { buildIssueMarkdownDocument } from './markdown.js'; document.addEventListener('DOMContentLoaded', () => { loadProducts(); setupEventListeners(); restoreFormState(); }); let activeDownloadUrl = null; const PROCESS_FORM_STATE_KEY = 'bugmedley.process.formState.v1'; const ISSUE_ID_PATTERN = /\b((?:[A-Z]{2,6}|WF500)-\d{4,8})\b/i; const BLOCK_CONTAINER_TAGS = new Set(['ARTICLE', 'ASIDE', 'BLOCKQUOTE', 'DIV', 'P', 'PRE', 'SECTION']); const RESOLVED_LINE_PATTERN = /^resolved\s+in\b/i; function loadProducts() { fetch('data/products.json') .then(response => response.json()) .then(data => { const productSelect = document.getElementById('productSelect'); const products = Object.keys(data || {}); products.forEach(product => { const option = document.createElement('option'); option.value = product; option.textContent = product; productSelect.appendChild(option); }); applyPersistedProductSelection(); }) .catch(error => { console.error('Error loading products:', error); setParseStatus('Could not load data/products.json', true); }); } function setupEventListeners() { document.getElementById('issuesInput').addEventListener('input', generateJSON); document.getElementById('productSelect').addEventListener('change', generateJSON); document.getElementById('issueTypeSelect').addEventListener('change', generateJSON); document.getElementById('versionInput').addEventListener('input', generateJSON); document.getElementById('copyMarkdownBtn').addEventListener('click', copyToClipboard); document.getElementById('productSelect').addEventListener('change', persistFormState); document.getElementById('issueTypeSelect').addEventListener('change', persistFormState); document.getElementById('versionInput').addEventListener('input', persistFormState); } function generateJSON() { const product = document.getElementById('productSelect').value; const issueType = document.getElementById('issueTypeSelect').value; const version = document.getElementById('versionInput').value.trim(); const inputText = document.getElementById('issuesInput').value; persistFormState(); if (!product || !issueType || !version || !inputText.trim()) { document.getElementById('markdownOutput').value = ''; resetDownloadLink(); setParseStatus(''); return; } const parsedIssues = parseIssuesFromHtmlTable(inputText); if (parsedIssues.length === 0) { document.getElementById('markdownOutput').value = ''; resetDownloadLink(); setParseStatus('No issues found in pasted input. Make sure it includes Issue ID and Description rows.', true); return; } const issues = parsedIssues.map(issue => ({ id: issue.id, description: issue.description, resolved: issue.resolved || '', caveat: issue.caveat || '' })); const markdownText = buildIssueMarkdownDocument({ type: issueType, product, version, issues }); document.getElementById('markdownOutput').value = markdownText; updateDownloadLink(markdownText, version); setParseStatus(`Parsed ${issues.length} issues from input.`); } function parseIssuesFromHtmlTable(htmlText) { const parser = new DOMParser(); const doc = parser.parseFromString(htmlText, 'text/html'); const table = doc.querySelector('table'); if (!table) { return []; } const rows = Array.from(table.querySelectorAll('tr')); const issues = []; rows.forEach(row => { const cells = Array.from(row.children).filter(cell => { const tag = (cell.tagName || '').toUpperCase(); return tag === 'TD' || tag === 'TH'; }); if (cells.length < 2) { return; } const leftCellText = extractCellText(cells[0]); const issueDetails = extractIssueDetails(cells[1]); const rightCellText = issueDetails.description; if (/^issue\s*id$/i.test(leftCellText) || /^description$/i.test(rightCellText)) { return; } const issueIdMatch = leftCellText.match(ISSUE_ID_PATTERN); if (!issueIdMatch) { return; } const id = issueIdMatch[1].toUpperCase(); const leftCellTrailingText = normalizeWhitespace( leftCellText.replace(issueIdMatch[0], '').replace(/^[-:;,.\s]+/, '') ); const resolved = RESOLVED_LINE_PATTERN.test(leftCellTrailingText) ? leftCellTrailingText : ''; const description = rightCellText; if (!description) { return; } issues.push({ id, description, resolved, caveat: issueDetails.caveat || (resolved ? '' : leftCellTrailingText) }); }); return issues; } function extractIssueDetails(cell) { const caveatNode = cell.querySelector('tt'); const caveat = caveatNode ? normalizeWhitespace(caveatNode.textContent || '') : ''; if (!caveatNode) { return { description: extractCellText(cell), caveat: '' }; } const clone = cell.cloneNode(true); const cloneCaveatNode = clone.querySelector('tt'); if (cloneCaveatNode) { cloneCaveatNode.remove(); } let description = extractCellText(clone); description = description.replace(/^\(\s*\)\s*/, '').trim(); return { description, caveat }; } function extractCellText(cell) { const clone = cell.cloneNode(true); return collectMarkdownBlocks(clone) .join('\n\n') .replace(/\n{3,}/g, '\n\n') .trim(); } function collectMarkdownBlocks(root, nestedListDepth = 0) { const blocks = []; let paragraphBuffer = ''; const flushParagraph = () => { const text = normalizeInlineMarkdown(paragraphBuffer); if (text) { blocks.push(text); } paragraphBuffer = ''; }; Array.from(root.childNodes).forEach(node => { if (node.nodeType === Node.TEXT_NODE) { paragraphBuffer += node.textContent || ''; return; } if (node.nodeType !== Node.ELEMENT_NODE) { return; } const tagName = node.tagName.toUpperCase(); if (tagName === 'BR') { paragraphBuffer += '\n'; return; } if (tagName === 'TABLE') { flushParagraph(); const tableMarkdown = convertHtmlTableToMarkdown(node); if (tableMarkdown) { blocks.push(tableMarkdown); } return; } if (tagName === 'UL' || tagName === 'OL') { flushParagraph(); const listMarkdown = convertHtmlListToMarkdown(node, nestedListDepth); if (listMarkdown) { blocks.push(listMarkdown); } return; } if (BLOCK_CONTAINER_TAGS.has(tagName)) { flushParagraph(); blocks.push(...collectMarkdownBlocks(node, nestedListDepth)); return; } paragraphBuffer += node.textContent || ''; }); flushParagraph(); return blocks; } function convertHtmlListToMarkdown(list, depth = 0) { const items = Array.from(list.children).filter(child => child.tagName && child.tagName.toUpperCase() === 'LI'); if (items.length === 0) { return ''; } const isOrdered = list.tagName.toUpperCase() === 'OL'; return items .map((item, index) => convertListItemToMarkdown(item, isOrdered ? `${index + 1}.` : '-', depth)) .filter(Boolean) .join('\n'); } function convertListItemToMarkdown(item, marker, depth) { const blocks = collectMarkdownBlocks(item, depth + 1); const indent = ' '.repeat(depth); const continuationIndent = `${indent} `; if (blocks.length === 0) { return `${indent}${marker}`; } const [firstBlock, ...remainingBlocks] = blocks; const firstLines = String(firstBlock || '').split('\n'); let output = `${indent}${marker} ${firstLines[0] || ''}`; if (firstLines.length > 1) { output += `\n${firstLines.slice(1).map(line => line ? `${continuationIndent}${line}` : continuationIndent.trimEnd()).join('\n')}`; } remainingBlocks.forEach(block => { if (!block) { return; } if (isIndentedListBlock(block, depth + 1)) { output += `\n${block}`; return; } output += `\n${block.split('\n').map(line => line ? `${continuationIndent}${line}` : '').join('\n')}`; }); return output.trimEnd(); } function isIndentedListBlock(block, depth) { const indent = ' '.repeat(depth); const lines = String(block || '').split('\n').filter(Boolean); return lines.length > 0 && lines.every(line => line.startsWith(indent) && /^\s*(?:-|\d+\.)\s/.test(line)); } function normalizeInlineMarkdown(value) { return String(value || '') .replace(/\u00a0/g, ' ') .replace(/[\t\f\v\r ]+/g, ' ') .replace(/ *\n+ */g, '\n') .trim(); } function convertHtmlTableToMarkdown(table) { const rows = Array.from(table.querySelectorAll('tr')) .map(row => Array.from(row.querySelectorAll('th, td'))) .filter(cells => cells.length > 0); if (rows.length === 0) { return ''; } const headerCells = rows[0].map(cell => sanitizeTableCellText(cell.textContent || '')); const columnCount = headerCells.length; const lines = []; lines.push(`| ${headerCells.join(' | ')} |`); lines.push(`| ${Array(columnCount).fill('---').join(' | ')} |`); rows.slice(1).forEach(cells => { const values = cells.map(cell => sanitizeTableCellText(cell.textContent || '')); while (values.length < columnCount) { values.push(''); } lines.push(`| ${values.slice(0, columnCount).join(' | ')} |`); }); return lines.join('\n'); } function sanitizeTableCellText(text) { return normalizeWhitespace(text).replace(/\|/g, '\\|'); } function normalizeWhitespace(value) { return value.replace(/\s+/g, ' ').trim(); } function setParseStatus(message, isError = false) { const status = document.getElementById('parseStatus'); status.textContent = message; status.style.color = isError ? '#b00020' : '#333'; } function copyToClipboard(event) { const markdownOutput = document.getElementById('markdownOutput'); if (!markdownOutput.value) { setParseStatus('Nothing to copy yet.', true); return; } navigator.clipboard.writeText(markdownOutput.value) .then(() => { const button = event && event.target ? event.target : null; if (!button) { setParseStatus('Copied to clipboard.'); return; } const originalText = button.textContent; button.textContent = 'Copied!'; setTimeout(() => { button.textContent = originalText; }, 1200); }) .catch(() => { setParseStatus('Clipboard copy failed. Copy manually from preview.', true); }); } function persistFormState() { const formState = { product: document.getElementById('productSelect').value || '', issueType: document.getElementById('issueTypeSelect').value || '', version: document.getElementById('versionInput').value || '' }; try { localStorage.setItem(PROCESS_FORM_STATE_KEY, JSON.stringify(formState)); } catch (error) { console.warn('Could not persist process form state:', error); } } function restoreFormState() { try { const raw = localStorage.getItem(PROCESS_FORM_STATE_KEY); if (!raw) { return; } const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') { return; } const productSelect = document.getElementById('productSelect'); const issueTypeSelect = document.getElementById('issueTypeSelect'); const versionInput = document.getElementById('versionInput'); if (typeof parsed.issueType === 'string') { issueTypeSelect.value = parsed.issueType; } if (typeof parsed.version === 'string') { versionInput.value = parsed.version; } if (typeof parsed.product === 'string' && productSelect.options.length > 1) { const optionExists = Array.from(productSelect.options).some(option => option.value === parsed.product); if (optionExists) { productSelect.value = parsed.product; } } } catch (error) { console.warn('Could not restore process form state:', error); } } function applyPersistedProductSelection() { try { const raw = localStorage.getItem(PROCESS_FORM_STATE_KEY); if (!raw) { return; } const parsed = JSON.parse(raw); if (!parsed || typeof parsed.product !== 'string') { return; } const productSelect = document.getElementById('productSelect'); const optionExists = Array.from(productSelect.options).some(option => option.value === parsed.product); if (optionExists) { productSelect.value = parsed.product; } } catch (error) { console.warn('Could not apply persisted product selection:', error); } } function updateDownloadLink(markdownText, version) { const link = document.getElementById('downloadMarkdownLink'); resetDownloadLink(); const blob = new Blob([markdownText], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); activeDownloadUrl = url; const dateStamp = new Date().toISOString().slice(0, 10); const safeVersion = String(version || 'version').replace(/[^a-zA-Z0-9.-]/g, '-'); link.href = url; link.download = `${safeVersion}_${dateStamp}.md`; link.style.pointerEvents = 'auto'; link.style.opacity = '1'; } function resetDownloadLink() { const link = document.getElementById('downloadMarkdownLink'); if (activeDownloadUrl) { URL.revokeObjectURL(activeDownloadUrl); activeDownloadUrl = null; } link.href = '#'; link.style.pointerEvents = 'none'; link.style.opacity = '0.6'; }