// --- CIDR Calculation Core Engine ---
/**
* Converts IP dotted decimal string to 32-bit unsigned integer.
*/
function ipToInt(ip) {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
}
/**
* Converts 32-bit unsigned integer to IP dotted decimal string.
*/
function intToIp(int) {
return [
(int >>> 24) & 255,
(int >>> 16) & 255,
(int >>> 8) & 255,
int & 255
].join('.');
}
/**
* Checks if the IP is in private ranges:
* - 10.0.0.0/8
* - 172.16.0.0/12
* - 192.168.0.0/16
* - 127.0.0.0/8 (Loopback is technically not private in the RFC 1918 sense, but we can treat as private/internal)
*/
function checkPrivate(ipInt) {
const o1 = (ipInt >>> 24) & 255;
const o2 = (ipInt >>> 16) & 255;
if (o1 === 10) return true;
if (o1 === 172 && o2 >= 16 && o2 <= 31) return true;
if (o1 === 192 && o2 === 168) return true;
if (o1 === 127) return true;
return false;
}
/**
* Determines class of IP based on first octet.
*/
function getIpClass(ipInt) {
const o1 = (ipInt >>> 24) & 255;
if (o1 >= 0 && o1 <= 127) return 'Class A';
if (o1 >= 128 && o1 <= 191) return 'Class B';
if (o1 >= 192 && o1 <= 223) return 'Class C';
if (o1 >= 224 && o1 <= 239) return 'Class D (Multicast)';
return 'Class E (Experimental)';
}
/**
* Parses and validates an IPv4 CIDR string.
*/
function parseCIDR(cidrStr) {
const clean = cidrStr.trim();
const parts = clean.split('/');
if (parts.length > 2) return null;
const ipPart = parts[0];
const prefixPart = parts[1];
// Validate IP format
const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const match = ipPart.match(ipRegex);
if (!match) return null;
for (let i = 1; i <= 4; i++) {
const val = parseInt(match[i], 10);
if (val < 0 || val > 255) return null;
// Prevent leading zero issues like 192.168.01.1
if (match[i].length > 1 && match[i].startsWith('0')) return null;
}
let prefix = 24;
if (prefixPart !== undefined) {
if (!/^\d+$/.test(prefixPart)) return null;
prefix = parseInt(prefixPart, 10);
if (prefix < 0 || prefix > 32) return null;
}
return { ip: ipPart, prefix };
}
/**
* Runs calculations on valid input.
*/
function calculateCIDR(ip, prefix) {
const ipVal = ipToInt(ip);
const maskVal = (prefix === 0 ? 0 : (~0 << (32 - prefix))) >>> 0;
const networkVal = (ipVal & maskVal) >>> 0;
const broadcastVal = (networkVal | ~maskVal) >>> 0;
let usableHosts = 0;
let firstHostVal = 0;
let lastHostVal = 0;
if (prefix === 32) {
usableHosts = 1;
firstHostVal = networkVal;
lastHostVal = networkVal;
} else if (prefix === 31) {
usableHosts = 2; // RFC 3021
firstHostVal = networkVal;
lastHostVal = broadcastVal;
} else {
usableHosts = Math.max(0, (broadcastVal - networkVal) - 1);
firstHostVal = networkVal + 1;
lastHostVal = broadcastVal - 1;
}
const wildcardVal = (~maskVal) >>> 0;
return {
cidrNotation: `${intToIp(networkVal)}/${prefix}`,
networkAddress: intToIp(networkVal),
broadcastAddress: intToIp(broadcastVal),
subnetMask: intToIp(maskVal),
wildcardMask: intToIp(wildcardVal),
usableHosts: usableHosts.toLocaleString(),
hostRange: usableHosts > 0 ? `${intToIp(firstHostVal)} – ${intToIp(lastHostVal)}` : 'N/A',
ipClass: getIpClass(ipVal),
isPrivate: checkPrivate(ipVal) ? 'Private (RFC 1918)' : 'Public'
};
}
// --- UI and Controller Layer ---
document.addEventListener('DOMContentLoaded', () => {
const cidrInput = document.getElementById('cidr-input');
const prefixDropdown = document.getElementById('prefix-dropdown');
const errorMsg = document.getElementById('error-msg');
const resultsContainer = document.getElementById('results');
// Value DOM fields
const outCidr = document.getElementById('out-cidr');
const outNetwork = document.getElementById('out-network');
const outBroadcast = document.getElementById('out-broadcast');
const outMask = document.getElementById('out-mask');
const outRange = document.getElementById('out-range');
const outHosts = document.getElementById('out-hosts');
const outClass = document.getElementById('out-class');
const outType = document.getElementById('out-type');
// Setup dropdown options (0 to 32)
for (let i = 0; i <= 32; i++) {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = `/${i}`;
if (i === 24) opt.selected = true;
prefixDropdown.appendChild(opt);
}
// Live calculation function
function update() {
let rawVal = cidrInput.value.trim();
if (!rawVal) {
clearResults();
hideError();
return;
}
// Auto-sync prefix dropdown if typed/pasted with a slash, then clean the input field to show only the IP
if (rawVal.includes('/')) {
const parts = rawVal.split('/');
const ipPart = parts[0].trim();
const prefixPart = parts[1].trim();
if (/^\d+$/.test(prefixPart)) {
const prefixVal = parseInt(prefixPart, 10);
if (prefixVal >= 0 && prefixVal <= 32) {
prefixDropdown.value = prefixVal;
}
}
cidrInput.value = ipPart;
rawVal = ipPart;
}
// Validate IP format
const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const match = rawVal.match(ipRegex);
let isValidIp = true;
if (!match) {
isValidIp = false;
} else {
for (let i = 1; i <= 4; i++) {
const val = parseInt(match[i], 10);
if (val < 0 || val > 255) isValidIp = false;
if (match[i].length > 1 && match[i].startsWith('0')) isValidIp = false;
}
}
if (!isValidIp) {
showError('Invalid IPv4 address. Use format: e.g. 192.168.0.1');
clearResults();
return;
}
hideError();
const prefix = parseInt(prefixDropdown.value, 10);
const results = calculateCIDR(rawVal, prefix);
displayResults(results);
}
function displayResults(data) {
outCidr.textContent = data.cidrNotation;
outNetwork.textContent = data.networkAddress;
outBroadcast.textContent = data.broadcastAddress;
outMask.textContent = data.subnetMask;
outRange.textContent = data.hostRange;
outHosts.textContent = data.usableHosts;
outClass.textContent = data.ipClass;
outType.textContent = data.isPrivate;
resultsContainer.classList.add('visible');
}
function clearResults() {
resultsContainer.classList.remove('visible');
}
function showError(msg) {
cidrInput.classList.add('input-error');
errorMsg.textContent = msg;
errorMsg.classList.add('visible');
}
function hideError() {
cidrInput.classList.remove('input-error');
errorMsg.classList.remove('visible');
}
// Listeners
cidrInput.addEventListener('input', update);
prefixDropdown.addEventListener('change', update);
// Copy to Clipboard
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => {
const targetId = btn.getAttribute('data-target');
const targetEl = document.getElementById(targetId);
if (!targetEl) return;
const text = targetEl.textContent;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('copied');
setTimeout(() => {
btn.classList.remove('copied');
}, 1500);
});
});
});
// --- Theme Toggle FAB Controller ---
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
const themes = ['system', 'light', 'dark'];
let currentThemeIndex = 0; // Default to 'system'
function applyTheme(theme) {
document.documentElement.removeAttribute('data-theme');
if (theme === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
themeIcon.innerHTML = ``; // Monitor icon
} else if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
themeIcon.innerHTML = ``; // Sun icon
} else if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
themeIcon.innerHTML = ``; // Moon icon
}
}
// Handle system preference change dynamically if system theme is selected
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (themes[currentThemeIndex] === 'system') {
applyTheme('system');
}
});
themeToggle.addEventListener('click', () => {
currentThemeIndex = (currentThemeIndex + 1) % themes.length;
applyTheme(themes[currentThemeIndex]);
});
// Init theme and calculation
applyTheme('system');
update();
});
// --- PWA Service Worker Registration ---
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js')
.then(reg => console.log('Service worker registered successfully', reg.scope))
.catch(err => console.error('Service worker registration failed', err));
});
}