Spaces:
Running
Running
| // --- 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 = `<path d="M4 2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 2v10h16V4H4zm4 16h8v2H8v-2z" fill="currentColor"/>`; // Monitor icon | |
| } else if (theme === 'light') { | |
| document.documentElement.setAttribute('data-theme', 'light'); | |
| themeIcon.innerHTML = `<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>`; // Sun icon | |
| } else if (theme === 'dark') { | |
| document.documentElement.setAttribute('data-theme', 'dark'); | |
| themeIcon.innerHTML = `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="currentColor"/>`; // 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)); | |
| }); | |
| } | |