diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e380a1c..c2ada70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: schedule: - cron: '30 3 * * 0' +permissions: + contents: read + jobs: coverage: name: Code Coverage & Memory Check diff --git a/docs/index.html b/docs/index.html index 1ee668d..856a23b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1247,49 +1247,6 @@

Address Components (Hexadecimal)

}); } - /** - * Unified helper to create diagnostic output HTML for an address - */ - function createAddressDiagnostics(addr) { - // Create flags HTML - const flags = ` -
-
Has Port
-
Has CIDR Mask
-
IPv4 Embedded
-
IPv4 Compatible
-
- `; - - // Create metadata HTML - const metadata = ` -
-
-

Port

-
${addr.port !== null ? addr.port : '-'}
-
-
-

CIDR Mask

-
${addr.mask !== null ? `/${addr.mask}` : '-'}
-
-
-

Zone ID

-
${addr.zone || '-'}
-
-
- `; - - // Create components HTML - const componentsHTML = addr.components.map((component, i) => ` -
-
[${i}]
-
0x${addr.getComponentHex(i)}
-
- `).join(''); - - return { flags, metadata, componentsHTML }; - } - /** * Parse and display address in single mode */ @@ -1317,12 +1274,17 @@

Zone ID

// Display formatted address document.getElementById('formatted').textContent = addr.formatted; - // Use unified diagnostic helper - const diagnostics = createAddressDiagnostics(addr); - // Update flags (higher in hierarchy now) const flagsContainer = results.querySelector('.flags'); - flagsContainer.innerHTML = diagnostics.flags.match(/
`; - errorHtml += `Input: "${err.input}"
`; + const errorHeading = document.createElement('strong'); + errorHeading.textContent = `❌ ${err.message}`; + + const errorDetails = document.createElement('div'); + errorDetails.style.cssText = "margin-top: 10px; font-family: 'Courier New', monospace;"; + errorDetails.append(document.createTextNode(`Input: "${err.input}"`), document.createElement('br')); + if (err.position !== undefined) { - errorHtml += `Error at position ${err.position}:
`; - // Show input with proper monospace alignment - errorHtml += '
'; - // Line 1: Input with highlighted error character - errorHtml += '' + err.input.substring(0, err.position) + ''; + errorDetails.append( + document.createTextNode(`Error at position ${err.position}:`), + document.createElement('br') + ); + + const inputDiagnostic = document.createElement('div'); + inputDiagnostic.style.cssText = 'line-height: 1.8; white-space: pre; font-family: monospace;'; + + const inputPrefix = document.createElement('span'); + inputPrefix.style.color = '#666'; + inputPrefix.textContent = err.input.substring(0, err.position); + inputDiagnostic.appendChild(inputPrefix); + if (err.position < err.input.length) { - errorHtml += '' + err.input.charAt(err.position) + ''; - errorHtml += '' + err.input.substring(err.position + 1) + ''; + const inputError = document.createElement('span'); + inputError.style.cssText = 'color: #fff; background: #dc3545; font-weight: bold;'; + inputError.textContent = err.input.charAt(err.position); + + const inputSuffix = document.createElement('span'); + inputSuffix.style.color = '#666'; + inputSuffix.textContent = err.input.substring(err.position + 1); + inputDiagnostic.append(inputError, inputSuffix); } - errorHtml += '\n'; - // Line 2: Pointer directly under the error character - errorHtml += '' + ' '.repeat(err.position) + '▲'; - errorHtml += '
'; + + const errorPointer = document.createElement('span'); + errorPointer.style.cssText = 'color: #dc3545; font-weight: bold;'; + errorPointer.textContent = `${' '.repeat(err.position)}▲`; + inputDiagnostic.append(document.createTextNode('\n'), errorPointer); + errorDetails.appendChild(inputDiagnostic); } - errorHtml += `
`; - status.innerHTML = errorHtml; + + status.replaceChildren(errorHeading, document.createElement('br'), errorDetails); } else { status.textContent = `❌ ${err.message}`; } @@ -1379,7 +1370,7 @@

Zone ID

// Clear any previous results const resultsContainer = document.getElementById('batch-results'); - resultsContainer.innerHTML = ''; + resultsContainer.replaceChildren(); // Focus the textarea batchInput.focus(); @@ -1396,7 +1387,9 @@

Zone ID

const resultsContainer = document.getElementById('batch-results'); if (!input) { - resultsContainer.innerHTML = '
❌ Please enter at least one address
'; + resultsContainer.replaceChildren( + createTextElement('div', 'status error', '❌ Please enter at least one address') + ); return; } @@ -1406,7 +1399,9 @@

Zone ID

.filter(line => line.length > 0); if (addresses.length === 0) { - resultsContainer.innerHTML = '
❌ No valid addresses found
'; + resultsContainer.replaceChildren( + createTextElement('div', 'status error', '❌ No valid addresses found') + ); return; } @@ -1425,101 +1420,93 @@

Zone ID

const failureCount = results.length - successCount; // Build summary cards - let summaryHTML = '
'; + const summary = document.createElement('div'); + summary.className = 'batch-summary'; if (successCount > 0) { - summaryHTML += ` -
-
-
-
${successCount}
-
Valid
-
-
- `; + summary.appendChild(createSummaryCard('success', '✓', successCount, 'Valid', '#0d5c2d')); } if (failureCount > 0) { - summaryHTML += ` -
-
-
-
${failureCount}
-
Invalid
-
-
- `; + summary.appendChild(createSummaryCard('error', '✗', failureCount, 'Invalid', '#a91429')); } - summaryHTML += '
'; // Build table - let tableHTML = ` -
-
-
#
-
Address
-
Status
-
Properties
-
-
- `; + const table = document.createElement('div'); + table.className = 'batch-table'; + + const tableHeader = document.createElement('div'); + tableHeader.className = 'batch-table-header'; + ['#', 'Address', 'Status', 'Properties', ''].forEach(label => { + tableHeader.appendChild(createTextElement('div', '', label)); + }); + table.appendChild(tableHeader); results.forEach((result, idx) => { const statusClass = result.success ? 'success' : 'error'; const statusIcon = result.success ? '✓' : '✗'; const statusText = result.success ? 'Valid' : 'Invalid'; + const row = document.createElement('div'); + row.className = `batch-table-row ${statusClass}`; + row.dataset.index = idx; + row.appendChild(createTextElement('div', 'batch-row-number', idx + 1)); + row.appendChild(createTextElement('div', 'batch-row-address', result.input)); + + const rowStatus = document.createElement('div'); + rowStatus.className = `batch-row-status ${statusClass}`; + rowStatus.append( + createTextElement('span', 'batch-row-status-icon', statusIcon), + document.createTextNode(statusText) + ); + row.appendChild(rowStatus); + + const propertiesColumn = document.createElement('div'); + // Generate property badges for successful parses - let propertiesHTML = ''; if (result.success) { - const props = []; + const properties = document.createElement('div'); + properties.className = 'batch-row-properties'; const data = result.data; // Determine IP type if (data.isIPv4Compatible || data.isIPv4Embed) { if (data.isIPv4Embed) { - props.push('IPv4-Mapped'); + properties.appendChild(createTextElement('span', 'batch-prop-badge embed', 'IPv4-Mapped')); } else { - props.push('IPv4'); + properties.appendChild(createTextElement('span', 'batch-prop-badge ipv4', 'IPv4')); } } else { - props.push('IPv6'); + properties.appendChild(createTextElement('span', 'batch-prop-badge ipv6', 'IPv6')); } // Add feature badges if (data.port !== null) { - props.push(`:${data.port}`); + properties.appendChild(createTextElement('span', 'batch-prop-badge port', `:${data.port}`)); } if (data.mask !== null) { - props.push(`/${data.mask}`); + properties.appendChild(createTextElement('span', 'batch-prop-badge mask', `/${data.mask}`)); } if (data.zone) { - props.push(`%${data.zone}`); + properties.appendChild(createTextElement('span', 'batch-prop-badge zone', `%${data.zone}`)); } - propertiesHTML = `
${props.join('')}
`; + propertiesColumn.appendChild(properties); } - tableHTML += ` -
-
${idx + 1}
-
${result.input}
-
- ${statusIcon} - ${statusText} -
-
${propertiesHTML}
-
-
-
-
- ${generateDetailHTML(result, idx)} -
-
- `; - }); + row.append(propertiesColumn, createTextElement('div', 'batch-row-expand', '▼')); - tableHTML += '
'; + const detail = document.createElement('div'); + detail.className = 'batch-detail'; + detail.id = `batch-detail-${idx}`; - resultsContainer.innerHTML = summaryHTML + tableHTML; + const detailContent = document.createElement('div'); + detailContent.className = 'batch-detail-content'; + appendBatchDetail(detailContent, result); + detail.appendChild(detailContent); + + table.append(row, detail); + }); + + resultsContainer.replaceChildren(summary, table); // Add click handlers for expandable rows document.querySelectorAll('.batch-table-row').forEach(row => { @@ -1550,55 +1537,147 @@

Zone ID

} /** - * Generate detailed HTML for a single result (matching single address mode) + * Create an element with text content that is never interpreted as HTML */ - function generateDetailHTML(result, idx) { - if (result.success) { - const diag = createAddressDiagnostics(result.data); - return ` -
-

Formatted Address

-
${result.data.formatted}
-
+ function createTextElement(tagName, className, text) { + const element = document.createElement(tagName); + if (className) { + element.className = className; + } + element.textContent = String(text); + return element; + } -
-

Flags

- ${diag.flags} -
+ /** + * Create a summary card for batch results + */ + function createSummaryCard(statusClass, icon, count, label, color) { + const card = document.createElement('div'); + card.className = `batch-summary-card ${statusClass}`; + card.appendChild(createTextElement('div', 'batch-summary-icon', icon)); - ${diag.metadata} + const content = document.createElement('div'); + content.className = 'batch-summary-content'; -
-

Components

-
${diag.componentsHTML}
-
- `; + const countElement = createTextElement('div', 'batch-summary-number', count); + countElement.style.color = color; + const labelElement = createTextElement('div', 'batch-summary-label', label); + labelElement.style.color = color; + content.append(countElement, labelElement); + card.appendChild(content); + return card; + } + + /** + * Append detailed output for a single batch result + */ + function appendBatchDetail(container, result) { + if (result.success) { + const data = result.data; + const formatted = document.createElement('div'); + formatted.className = 'result-item'; + formatted.style.marginBottom = '20px'; + formatted.append( + createTextElement('h3', '', 'Formatted Address'), + createTextElement('div', 'value', data.formatted) + ); + container.appendChild(formatted); + + const flagsSection = document.createElement('div'); + flagsSection.style.marginBottom = '20px'; + const flagsHeading = createTextElement('h3', '', 'Flags'); + flagsHeading.style.cssText = 'margin-bottom: 12px; font-size: 0.9em; color: #666; text-transform: uppercase; letter-spacing: 0.5px;'; + + const flags = document.createElement('div'); + flags.className = 'flags'; + [ + ['Has Port', data.hasPort], + ['Has CIDR Mask', data.hasMask], + ['IPv4 Embedded', data.isIPv4Embedded], + ['IPv4 Compatible', data.isIPv4Compatible] + ].forEach(([label, enabled]) => { + flags.appendChild(createTextElement('div', `flag${enabled ? '' : ' disabled'}`, label)); + }); + flagsSection.append(flagsHeading, flags); + container.appendChild(flagsSection); + + const metadata = document.createElement('div'); + metadata.className = 'result-grid'; + [ + ['Port', data.port !== null ? data.port : '-'], + ['CIDR Mask', data.mask !== null ? `/${data.mask}` : '-'], + ['Zone ID', data.zone || '-'] + ].forEach(([label, value]) => { + const item = document.createElement('div'); + item.className = 'result-item'; + item.append(createTextElement('h3', '', label), createTextElement('div', 'value', value)); + metadata.appendChild(item); + }); + container.appendChild(metadata); + + const componentsSection = document.createElement('div'); + componentsSection.style.marginTop = '24px'; + const componentsHeading = createTextElement('h3', '', 'Components'); + componentsHeading.style.cssText = 'margin-bottom: 12px; font-size: 0.9em; color: #666; text-transform: uppercase; letter-spacing: 0.5px;'; + + const components = document.createElement('div'); + components.className = 'components'; + data.components.forEach((_, index) => { + const item = document.createElement('div'); + item.className = 'component'; + item.append( + createTextElement('div', 'label', `[${index}]`), + createTextElement('div', 'hex', `0x${data.getComponentHex(index)}`) + ); + components.appendChild(item); + }); + componentsSection.append(componentsHeading, components); + container.appendChild(componentsSection); } else { // Error diagnostics - let errorHTML = `
❌ ${result.error.message || 'Parse error'}
`; + const errorHeading = document.createElement('div'); + errorHeading.style.marginBottom = '16px'; + const errorText = createTextElement('strong', '', `❌ ${result.error.message || 'Parse error'}`); + errorText.style.cssText = 'color: #a91429; font-size: 1.1em;'; + errorHeading.appendChild(errorText); + container.appendChild(errorHeading); if (result.error.diagnostic && result.error.diagnostic.message) { - errorHTML += `
${result.error.diagnostic.message}
`; + const diagnosticMessage = createTextElement('div', '', result.error.diagnostic.message); + diagnosticMessage.style.cssText = 'margin-bottom: 12px; color: #721c24; font-weight: 500;'; + container.appendChild(diagnosticMessage); if (result.error.position !== undefined) { - errorHTML += `
`; - errorHTML += `
Error at position ${result.error.position}:
`; - errorHTML += '
'; + const diagnostic = document.createElement('div'); + diagnostic.style.cssText = "margin-top: 16px; font-family: 'SF Mono', 'Consolas', 'Monaco', monospace; font-size: 0.95em;"; + + const position = createTextElement('div', '', `Error at position ${result.error.position}:`); + position.style.cssText = 'margin-bottom: 8px; color: #666;'; + + const inputDiagnostic = document.createElement('div'); + inputDiagnostic.style.cssText = 'line-height: 1.8; white-space: pre; background: #fff; padding: 12px; border-radius: 8px; border: 2px solid #ffc4cd;'; // Show input with highlighted error character - errorHTML += '' + result.input.substring(0, result.error.position) + ''; + const inputPrefix = createTextElement('span', '', result.input.substring(0, result.error.position)); + inputPrefix.style.color = '#666'; + inputDiagnostic.appendChild(inputPrefix); + if (result.error.position < result.input.length) { - errorHTML += '' + result.input.charAt(result.error.position) + ''; - errorHTML += '' + result.input.substring(result.error.position + 1) + ''; + const inputError = createTextElement('span', '', result.input.charAt(result.error.position)); + inputError.style.cssText = 'color: #fff; background: #dc3545; font-weight: bold;'; + const inputSuffix = createTextElement('span', '', result.input.substring(result.error.position + 1)); + inputSuffix.style.color = '#666'; + inputDiagnostic.append(inputError, inputSuffix); } - errorHTML += '\n'; + // Pointer line underneath - errorHTML += '' + ' '.repeat(result.error.position) + '▲'; - errorHTML += '
'; + const pointer = createTextElement('span', '', `${' '.repeat(result.error.position)}▲`); + pointer.style.cssText = 'color: #dc3545; font-weight: bold;'; + inputDiagnostic.append(document.createTextNode('\n'), pointer); + diagnostic.append(position, inputDiagnostic); + container.appendChild(diagnostic); } } - - return errorHTML; } }