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 = `
-
-
- `;
+ 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;
}
}