1/* Toolbench â per-tool runtime. Each function gets the tool host element. 2 Registered under window.ToolbenchTools[<runtime name>]. */ 3(function () { 4 "use strict"; 5 var T = (window.ToolbenchTools = window.ToolbenchTools || {}); 6 var $ = function (id) { return document.getElementById(id); }; 7 var fmt = function (n, dp) { return window.Toolbench.fmt(n, dp); }; 8 var copy = function (t, l) { window.Toolbench.copy(t, l); }; 9 function on(el, ev, fn) { if (el) el.addEventListener(ev, fn); } 10 function activeTab(panel) { 11 var t = panel.querySelector(".tp-tab.is-active"); 12 return t ? t.getAttribute("data-tab") : null; 13 } 14 function num(id) { var v = parseFloat($(id) && $(id).value); return isFinite(v) ? v : 0; } 15 16 /* ââââââââ CALCULATORS ââââââââ */ 17 T.percentageCalculator = function (panel) { 18 var out = $("pctResult"); 19 function calc() { 20 var tab = activeTab(panel), r; 21 if (tab === "of") { r = (num("pofA") / 100) * num("pofB"); out.textContent = fmt(r, 2); } 22 else if (tab === "is") { 23 var b = num("pisB"); r = b ? (num("pisA") / b) * 100 : 0; 24 out.textContent = fmt(r, 2) + " %"; 25 } else { 26 var a = num("pcA"), c = num("pcB"); r = a ? ((c - a) / a) * 100 : 0; 27 out.innerHTML = fmt(Math.abs(r), 2) + " %<small>" + (r >= 0 ? "increase" : "decrease") + "</small>"; 28 } 29 } 30 panel.addEventListener("input", calc); 31 panel.addEventListener("tabchange", calc); 32 calc(); 33 }; 34 35 T.bmiCalculator = function (panel) { 36 var out = $("bmiResult"); 37 function band(b) { 38 if (b < 18.5) return "Underweight"; 39 if (b < 25) return "Healthy weight"; 40 if (b < 30) return "Overweight"; 41 return "Obese"; 42 } 43 function calc() { 44 var tab = activeTab(panel), bmi; 45 if (tab === "metric") { 46 var h = num("bmiHcm") / 100, w = num("bmiWkg"); 47 bmi = h > 0 ? w / (h * h) : 0; 48 } else { 49 var inches = num("bmiHft") * 12 + num("bmiHin"), lb = num("bmiWlb"); 50 bmi = inches > 0 ? (703 * lb) / (inches * inches) : 0; 51 } 52 if (!bmi) { out.textContent = "â"; return; } 53 out.innerHTML = fmt(bmi, 1) + "<small>" + band(bmi) + "</small>"; 54 } 55 panel.addEventListener("input", calc); 56 panel.addEventListener("tabchange", calc); 57 calc(); 58 }; 59 60 T.loanEmiCalculator = function () { 61 var out = $("emiResult"), bd = $("emiBreakdown"); 62 function calc() { 63 var P = num("emiP"), rate = num("emiR"), yrs = num("emiN"); 64 var n = yrs * 12, r = rate / 12 / 100, emi; 65 if (r === 0) emi = n ? P / n : 0; 66 else emi = (P * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1); 67 var total = emi * n, interest = total - P; 68 if (!isFinite(emi) || emi <= 0) { out.textContent = "â"; bd.innerHTML = ""; return; } 69 out.innerHTML = fmt(emi, 2) + "<small>per month</small>"; 70 bd.innerHTML = 71 item("Total interest", fmt(interest, 2)) + 72 item("Total repayment", fmt(total, 2)) + 73 item("Payments", fmt(n, 0)); 74 } 75 on($("emiP"), "input", calc); on($("emiR"), "input", calc); on($("emiN"), "input", calc); 76 calc(); 77 }; 78 79 T.ageCalculator = function () { 80 var out = $("ageResult"), bd = $("ageBreakdown"), on2 = $("ageOn"); 81 on2.value = new Date().toISOString().slice(0, 10); 82 function calc() { 83 var dob = $("ageDob").value, end = on2.value; 84 if (!dob || !end) { out.textContent = "â"; bd.innerHTML = ""; return; } 85 var d1 = new Date(dob), d2 = new Date(end); 86 if (d2 < d1) { out.textContent = "Second date is before the birth date"; bd.innerHTML = ""; return; } 87 var y = d2.getFullYear() - d1.getFullYear(); 88 var m = d2.getMonth() - d1.getMonth(); 89 var d = d2.getDate() - d1.getDate(); 90 if (d < 0) { m--; d += new Date(d2.getFullYear(), d2.getMonth(), 0).getDate(); } 91 if (m < 0) { y--; m += 12; } 92 var totalDays = Math.floor((d2 - d1) / 86400000); 93 out.innerHTML = y + " yr " + m + " mo " + d + " d"; 94 var nb = new Date(d2.getFullYear(), d1.getMonth(), d1.getDate()); 95 if (nb < d2) nb.setFullYear(nb.getFullYear() + 1);
96 var untilNb = Math.ceil((nb - d2) / 86400000); 97 bd.innerHTML = 98 item("Total days", fmt(totalDays, 0)) + 99 item("Total weeks", fmt(Math.floor(totalDays / 7), 0)) + 100 item("Next birthday in", untilNb + " days"); 101 } 102 on($("ageDob"), "input", calc); on(on2, "input", calc); 103 calc(); 104 }; 105 106 T.discountCalculator = function () { 107 var out = $("discResult"); 108 function calc() { 109 var price = num("discPrice"), pct = num("discPct"); 110 var save = price * (pct / 100), final = price - save; 111 out.innerHTML = fmt(final, 2) + "<small>you save " + fmt(save, 2) + "</small>"; 112 } 113 on($("discPrice"), "input", calc); on($("discPct"), "input", calc); 114 calc(); 115 }; 116 117 /* ââââââââ DEVELOPER ââââââââ */ 118 T.jsonFormatter = function (panel) { 119 var inp = $("jsonInput"), out = $("jsonOutput"), st = $("jsonStatus"); 120 function indent() { var v = $("jsonIndent").value; return v === "tab" ? "\t" : parseInt(v, 10); } 121 function run(min) { 122 st.textContent = ""; st.className = "tp-status"; 123 if (!inp.value.trim()) { out.value = ""; return; } 124 try { 125 var obj = JSON.parse(inp.value); 126 out.value = min ? JSON.stringify(obj) : JSON.stringify(obj, null, indent()); 127 st.textContent = "\u2713 Valid JSON"; st.className = "tp-status ok"; 128 } catch (e) { 129 out.value = ""; st.textContent = "\u2717 " + e.message; st.className = "tp-status err"; 130 } 131 } 132 panel.querySelectorAll("[data-act]").forEach(function (b) { 133 on(b, "click", function () { 134 var a = b.getAttribute("data-act"); 135 if (a === "format") run(false); 136 else if (a === "minify") run(true); 137 else if (a === "copy") copy(out.value, "Copied output"); 138 else if (a === "clear") { inp.value = ""; out.value = ""; st.textContent = ""; } 139 }); 140 }); 141 on(inp, "input", function () { run(false); }); 142 }; 143 144 T.base64Tool = function (panel) { 145 var inp = $("b64Input"), out = $("b64Output"); 146 function enc(s) { return btoa(unescape(encodeURIComponent(s))); } 147 function dec(s) { return decodeURIComponent(escape(atob(s.replace(/\s/g, "")))); } 148 function run() { 149 var tab = activeTab(panel); 150 if (!inp.value) { out.value = ""; return; } 151 try { out.value = tab === "encode" ? enc(inp.value) : dec(inp.value); } 152 catch (e) { out.value = "\u26A0 Invalid Base64 input"; } 153 } 154 panel.addEventListener("input", run); 155 panel.addEventListener("tabchange", run); 156 panel.querySelectorAll("[data-act]").forEach(function (b) { 157 on(b, "click", function () { 158 var a = b.getAttribute("data-act"); 159 if (a === "copy") copy(out.value, "Copied result"); 160 else if (a === "clear") { inp.value = ""; out.value = ""; } 161 }); 162 }); 163 }; 164 165 T.urlTool = function (panel) { 166 var inp = $("urlInput"), out = $("urlOutput"), comp = $("urlComponent"); 167 function run() { 168 var tab = activeTab(panel); 169 if (!inp.value) { out.value = ""; return; } 170 try { 171 if (tab === "encode") out.value = comp.checked ? encodeURIComponent(inp.value) : encodeURI(inp.value); 172 else out.value = comp.checked ? decodeURIComponent(inp.value) : decodeURI(inp.value); 173 } catch (e) { out.value = "\u26A0 Could not decode input"; } 174 } 175 panel.addEventListener("input", run); 176 panel.addEventListener("tabchange", run); 177 panel.querySelectorAll("[data-act]").forEach(function (b) { 178 on(b, "click", function () { 179 var a = b.getAttribute("data-act"); 180 if (a === "copy") copy(out.value, "Copied result"); 181 else if (a === "clear") { inp.value = ""; out.value = ""; } 182 }); 183 }); 184 }; 185 186 T.uuidGenerator = function (panel) { 187 var out = $("uuidOutput"); 188 function uuid() { 189 if (crypto.randomUUID) return crypto.randomUUID(); 190 var b = crypto.getRandomValues(new Uint8Array(16)); 191 b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; 192 var h = [].map.call(b, function (x) { return ("0" + x.toString(16)).slice(-2); }); 193 return h.slice(0, 4).join("") + "-" + h.slice(4, 6).join("") + "-" + h.slice(6, 8).join("") + "-" + h.slice(8, 10).join("") + "-" + h.slice(10).join(""); 194 } 195 function gen() { 196 var count = Math.max(1, Math.min(1000, parseInt($("uuidCount").value, 10) || 1)); 197 var list = []; 198 for (var i = 0; i < count; i++) { 199 var u = uuid(); 200 if ($("uuidNoDash").checked) u = u.replace(/-/g, ""); 201 if ($("uuidUpper").checked) u = u.toUpperCase(); 202 list.push(u); 203 } 204 out.value = list.join("\n"); 205 } 206 panel.querySelectorAll("[data-act]").forEach(function (b) { 207 on(b, "click", function () { 208 var a = b.getAttribute("data-act"); 209 if (a === "generate") gen(); 210 else if (a === "copy") copy(out.value, "Copied UUIDs"); 211 }); 212 }); 213 gen(); 214 }; 215 216 T.hashGenerator = function (panel) { 217 var inp = $("hashInput"), box = $("hashOutputs"), last256 = ""; 218 var algos = [["SHA-256", "SHA-256"], ["SHA-1", "SHA-1"], ["SHA-512", "SHA-512"]]; 219 function hex(buf) { 220 return [].map.call(new Uint8Array(buf), function (x) { return ("0" + x.toString(16)).slice(-2); }).join(""); 221 } 222 async function run() { 223 if (!inp.value) { box.innerHTML = ""; last256 = ""; return; } 224 var data = new TextEncoder().encode(inp.value); 225 var rows = ""; 226 for (var i = 0; i < algos.length; i++) { 227 try { 228 var d = await crypto.subtle.digest(algos[i][1], data); 229 var h = hex(d); 230 if (algos[i][0] === "SHA-256") last256 = h; 231 rows += '<div class="hash-row"><span class="hr-label">' + algos[i][0] + '</span><span class="hr-val">' + h + "</span></div>"; 232 } catch (e) {} 233 } 234 box.innerHTML = rows; 235 } 236 on(inp, "input", run); 237 panel.querySelectorAll("[data-act]").forEach(function (b) { 238 on(b, "click", function () { if (last256) copy(last256, "Copied SHA-256"); }); 239 }); 240 }; 241 242 /* ââââââââ TEXT ââââââââ */ 243 T.wordCounter = function () { 244 var inp = $("wcInput"), grid = $("wcStats"); 245 function run() { 246 var t = inp.value; 247 var words = (t.trim().match(/\S+/g) || []).length; 248 var chars = t.length; 249 var noSpace = t.replace(/\s/g, "").length; 250 var sentences = (t.match(/[.!?]+(\s|$)/g) || []).length; 251 var paras = (t.trim() ? t.trim().split(/\n\s*\n/).length : 0); 252 var mins = Math.max(1, Math.round(words / 200)); 253 grid.innerHTML = 254 stat(fmt(words, 0), "Words") + stat(fmt(chars, 0), "Characters") + 255 stat(fmt(noSpace, 0), "No spaces") + stat(fmt(sentences, 0), "Sentences") + 256 stat(fmt(paras, 0), "Paragraphs") + stat("~" + mins, "Min read"); 257 } 258 on(inp, "input", run); run(); 259 }; 260 261 T.caseConverter = function (panel) { 262 var inp = $("caseInput"), out = $("caseOutput"); 263 function words(s) { return s.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().split(/\s+/); } 264 var conv = { 265 upper: function (s) { return s.toUpperCase(); }, 266 lower: function (s) { return s.toLowerCase(); }, 267 title: function (s) { return s.replace(/\w\S*/g, function (w) { return w[0].toUpperCase() + w.slice(1).toLowerCase(); }); }, 268 sentence: function (s) { return s.toLowerCase().replace(/(^\s*\w|[.!?]\s+\w)/g, function (c) { return c.toUpperCase(); }); }, 269 camel: function (s) { return words(s).map(function (w, i) { w = w.toLowerCase(); return i ? w[0].toUpperCase() + w.slice(1) : w; }).join(""); }, 270 snake: function (s) { return words(s).map(function (w) { return w.toLowerCase(); }).join("_"); }, 271 kebab: function (s) { return words(s).map(function (w) { return w.toLowerCase(); }).join("-"); }, 272 }; 273 panel.querySelectorAll("[data-case]").forEach(function (b) { 274 on(b, "click", function () { out.value = conv[b.getAttribute("data-case")](inp.value); }); 275 }); 276 panel.querySelectorAll("[data-act]").forEach(function (b) { 277 on(b, "click", function () { 278 var a = b.getAttribute("data-act"); 279 if (a === "copy") copy(out.value, "Copied"); 280 else if (a === "clear") { inp.value = ""; out.value = ""; } 281 }); 282 }); 283 }; 284 285 T.loremGenerator = function (panel) { 286 var out = $("loremOutput"); 287 var W = ("lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in reprehenderit voluptate velit esse cillum eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum").split(" "); 288 function rand(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; } 289 function word() { return W[rand(0, W.length - 1)]; } 290 function sentence() { 291 var n = rand(8, 16), s = []; 292 for (var i = 0; i < n; i++) s.push(word()); 293 s[0] = s[0][0].toUpperCase() + s[0].slice(1); 294 return s.join(" ") + "."; 295 } 296 function para() { var n = rand(3, 6), p = []; for (var i = 0; i < n; i++) p.push(sentence()); return p.join(" "); } 297 function gen() { 298 var count = Math.max(1, Math.min(100, parseInt($("loremCount").value, 10) || 1)); 299 var unit = $("loremUnit").value, parts = []; 300 if (unit === "paragraphs") { for (var i = 0; i < count; i++) parts.push(para()); } 301 else if (unit === "sentences") { var ss = []; for (var j = 0; j < count; j++) ss.push(sentence()); parts = [ss.join(" ")]; } 302 else { var w = []; for (var k = 0; k < count;
302 k++) w.push(word()); w[0] = w[0][0].toUpperCase() + w[0].slice(1); parts = [w.join(" ") + "."]; } 303 if ($("loremStart").checked && parts.length) parts[0] = "Lorem ipsum dolor sit amet, " + parts[0][0].toLowerCase() + parts[0].slice(1); 304 out.innerHTML = parts.map(function (p) { return "<p>" + p + "</p>"; }).join(""); 305 } 306 panel.querySelectorAll("[data-act]").forEach(function (b) { 307 on(b, "click", function () { 308 var a = b.getAttribute("data-act"); 309 if (a === "generate") gen(); 310 else if (a === "copy") copy(out.innerText, "Copied text"); 311 }); 312 }); 313 gen(); 314 }; 315 316 T.dedupeLines = function (panel) { 317 var inp = $("dedupInput"), out = $("dedupOutput"), st = $("dedupStatus"); 318 function run() { 319 var lines = inp.value.split("\n"); 320 var trim = $("dedupTrim").checked, ic = $("dedupCase").checked, blank = $("dedupBlank").checked; 321 var seen = {}, res = []; 322 lines.forEach(function (l) { 323 var v = trim ? l.trim() : l; 324 if (blank && v === "") return; 325 var key = ic ? v.toLowerCase() : v; 326 if (!seen[key]) { seen[key] = 1; res.push(v); } 327 }); 328 if ($("dedupSort").checked) res.sort(function (a, b) { return a.localeCompare(b); }); 329 out.value = res.join("\n"); 330 var removed = lines.length - res.length; 331 st.textContent = removed > 0 ? "Removed " + removed + " duplicate/blank line" + (removed === 1 ? "" : "s") + " \u00B7 " + res.length + " kept" : res.length + " unique lines"; 332 st.className = "tp-status ok"; 333 } 334 panel.querySelectorAll("[data-act]").forEach(function (b) { 335 on(b, "click", function () { 336 var a = b.getAttribute("data-act"); 337 if (a === "dedupe") run(); 338 else if (a === "copy") copy(out.value, "Copied"); 339 }); 340 }); 341 }; 342 343 T.slugGenerator = function (panel) { 344 var inp = $("slugInput"), out = $("slugResult"); 345 var STOP = "a an and the or but of in on at to for with is are".split(" "); 346 function run() { 347 var sep = $("slugSep").value; 348 var s = inp.value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); 349 s = s.replace(/[^a-z0-9\s-]/g, "").trim(); 350 var w = s.split(/[\s-]+/).filter(Boolean); 351 if ($("slugStop").checked) w = w.filter(function (x) { return STOP.indexOf(x) === -1; }); 352 out.textContent = w.join(sep) || "\u2014"; 353 } 354 panel.addEventListener("input", run); 355 panel.querySelectorAll("[data-act]").forEach(function (b) { 356 on(b, "click", function () { copy(out.textContent, "Copied slug"); }); 357 }); 358 run(); 359 }; 360 361 /* ââââââââ IMAGE ââââââââ */ 362 function wireDrop(dropId, fileId, onFiles) { 363 var dz = $(dropId), fi = $(fileId); 364 on(dz, "click", function () { fi.click(); }); 365 on(fi, "change", function () { if (fi.files.length) onFiles(fi.files); }); 366 ["dragover", "dragenter"].forEach(function (ev) { 367 on(dz, ev, function (e) { e.preventDefault(); dz.classList.add("drag"); }); 368 }); 369 ["dragleave", "drop"].forEach(function (ev) { 370 on(dz, ev, function (e) { e.preventDefault(); dz.classList.remove("drag"); }); 371 }); 372 on(dz, "drop", function (e) { if (e.dataTransfer.files.length) onFiles(e.dataTransfer.files); }); 373 } 374 function humanSize(b) { 375 if (b < 1024) return b + " B"; 376 if (b < 1048576) return (b / 1024).toFixed(1) + " KB"; 377 return (b / 1048576).toFixed(2) + " MB"; 378 } 379 function loadImage(file) { 380 return new Promise(function (res, rej) { 381 var img = new Image(); 382 img.onload = function () { res(img); }; 383 img.onerror = rej; 384 img.src = URL.createObjectURL(file); 385 }); 386 } 387 388 T.imageCompressor = function () { 389 var origImg = null, origSize = 0; 390 wireDrop("imgcDrop", "imgcFile", function (files) { 391 origSize = files[0].size; 392 loadImage(files[0]).then(function (img) { origImg = img; render(); }); 393 $("imgcControls").hidden = false; $("imgcActions").hidden = false; $("imgcPreview").hidden = false; 394 }); 395 function render() { 396 if (!origImg) return; 397 var q = parseFloat($("imgcQuality").value), fmtType = $("imgcFormat").value; 398 var c = document.createElement("canvas");
399 c.width = origImg.naturalWidth; c.height = origImg.naturalHeight; 400 c.getContext("2d").drawImage(origImg, 0, 0); 401 c.toBlob(function (blob) { 402 if (!blob) return; 403 var url = URL.createObjectURL(blob); 404 $("imgcPreview").innerHTML = 405 '<figure><img src="' + origImg.src + '"><figcaption>Original \u00B7 ' + humanSize(origSize) + "</figcaption></figure>" + 406 '<figure><img src="' + url + '"><figcaption>Compressed \u00B7 ' + humanSize(blob.size) + 407 " (" + Math.round((1 - blob.size / origSize) * 100) + "% smaller)</figcaption></figure>"; 408 var dl = $("imgcDownload"); dl.href = url; 409 dl.download = "compressed." + (fmtType.split("/")[1] === "jpeg" ? "jpg" : fmtType.split("/")[1]); 410 }, fmtType, q); 411 } 412 on($("imgcQuality"), "input", render); on($("imgcFormat"), "change", render); 413 }; 414 415 T.imageResizer = function () { 416 var img = null, ratio = 1; 417 function lock() { return $("imgrLock").checked; } 418 wireDrop("imgrDrop", "imgrFile", function (files) { 419 loadImage(files[0]).then(function (i) { 420 img = i; ratio = i.naturalWidth / i.naturalHeight; 421 $("imgrW").value = i.naturalWidth; $("imgrH").value = i.naturalHeight; 422 $("imgrControls").hidden = false; $("imgrActions").hidden = false; $("imgrPreview").hidden = false; 423 render(); 424 }); 425 }); 426 on($("imgrW"), "input", function () { if (lock() && img) $("imgrH").value = Math.round(num("imgrW") / ratio); render(); }); 427 on($("imgrH"), "input", function () { if (lock() && img) $("imgrW").value = Math.round(num("imgrH") * ratio); render(); }); 428 function render() { 429 if (!img) return; 430 var w = Math.max(1, num("imgrW")), h = Math.max(1, num("imgrH")); 431 var c = document.createElement("canvas"); c.width = w; c.height = h; 432 c.getContext("2d").drawImage(img, 0, 0, w, h); 433 c.toBlob(function (blob) { 434 if (!blob) return; 435 var url = URL.createObjectURL(blob); 436 $("imgrPreview").innerHTML = '<figure><img src="' + url + '"><figcaption>' + w + " \u00D7 " + h + " px \u00B7 " + humanSize(blob.size) + "</figcaption></figure>"; 437 $("imgrDownload").href = url; $("imgrDownload").download = "resized.png"; 438 }, "image/png"); 439 } 440 }; 441 442 T.imageToBase64 = function (panel) { 443 var dataUri = ""; 444 wireDrop("i2bDrop", "i2bFile", function (files) { 445 var fr = new FileReader(); 446 fr.onload = function () { 447 dataUri = fr.result; 448 $("i2bOutput").value = dataUri; 449 $("i2bPreview").hidden = false; $("i2bActions").hidden = false; 450 $("i2bPreview").innerHTML = '<figure><img src="' + dataUri + '"><figcaption>' + humanSize(files[0].size) + " \u2192 " + humanSize(dataUri.length) + " as text</figcaption></figure>"; 451 }; 452 fr.readAsDataURL(files[0]); 453 }); 454 panel.querySelectorAll("[data-act]").forEach(function (b) { 455 on(b, "click", function () { 456 var a = b.getAttribute("data-act"); 457 if (a === "copyData") copy(dataUri, "Copied data URI"); 458 else if (a === "copyImg") copy('<img src="' + dataUri + '" alt="">', "Copied img tag"); 459 else if (a === "copyCss") copy("background-image: url(" + dataUri + ");", "Copied CSS"); 460 }); 461 }); 462 }; 463 464 /* ââââââââ FINANCE ââââââââ */ 465 T.compoundInterest = function () { 466 var out = $("ciResult"), bd = $("ciBreakdown"); 467 function calc() { 468 var P = num("ciP"), pmt = num("ciPmt"), r = num("ciR") / 100, yrs = num("ciN"), n = parseInt($("ciFreq").value, 10); 469 var i = r / n, periods = n * yrs; 470 var fvP = P * Math.pow(1 + i, periods); 471 var monthlyPeriods = 12 * yrs, mi = r / 12; 472 var fvC = mi === 0 ? pmt * monthlyPeriods : pmt * ((Math.pow(1 + mi, monthlyPeriods) - 1) / mi); 473 var total = fvP + fvC; 474 var contributed = P + pmt * monthlyPeriods; 475 var interest = total - contributed; 476 if (!isFinite(total)) { out.textContent = "\u2014"; bd.innerHTML = ""; return; } 477 out.innerHTML = fmt(total, 2) + "<small>final balance after " + yrs + " years</small>"; 478 bd.innerHTML = 479 item("You contribute", fmt(contributed, 2)) + 480 item("Interest earned", fmt(interest, 2)) + 481 item("Growth multiple", (contributed ? total / contributed : 0).toFixed(2) + "\u00D7"); 482 } 483 ["ciP", "ciPmt", "ciR", "ciN"].forEach(function (id) { on($(id), "input", calc); }); 484 on($("ciFreq"), "change", calc); 485 calc(); 486 }; 487 488 T.simpleInterest = function () { 489 var out = $("siResult"); 490 function calc() { 491 var P = num("siP"), r = num("siR") / 100, t = num("siT"); 492 var I = P * r * t; 493 out.innerHTML = fmt(P + I, 2) + "<small>interest " + fmt(I, 2) + " over " + t + " years</small>"; 494 } 495 ["siP", "siR", "siT"].forEach(function (id) { on($(id), "input", calc); }); 496 calc(); 497 }; 498 499 T.savingsGoal = function () { 500 var out = $("sgResult"); 501 function calc() { 502 var goal = num("sgGoal"), start = num("sgStart"), months = num("sgMonths"), r = num("sgRate") / 100 / 12; 503 var fvStart = start * Math.pow(1 + r, months); 504 var need = goal - fvStart; 505 if (need <= 0) { out.innerHTML = "Goal already reached<small>your current savings will grow past the target</small>"; return; } 506 var pmt = r === 0 ? need / months : need * r / (Math.pow(1 + r, months) - 1); 507 out.innerHTML = fmt(pmt, 2) + "<small>per month for " + months + " months</small>"; 508 } 509 ["sgGoal", "sgStart", "sgMonths", "sgRate"].forEach(function (id) { on($(id), "input", calc); }); 510 calc(); 511 }; 512 513 T.roiCalculator = function () { 514 var out = $("roiResult"), bd = $("roiBreakdown"); 515 function calc() { 516 var cost = num("roiCost"), final = num("roiFinal"), yrs = num("roiYears"); 517 if (!cost) { out.textContent = "\u2014"; bd.innerHTML = ""; return; } 518 var gain = final - cost, roi = (gain / cost) * 100; 519 out.innerHTML = fmt(roi, 2) + " %<small>total return on investment</small>";
520 var ann = yrs > 0 ? (Math.pow(final / cost, 1 / yrs) - 1) * 100 : null; 521 bd.innerHTML = 522 item("Net profit", fmt(gain, 2)) + 523 (ann != null && isFinite(ann) ? item("Annualized ROI", fmt(ann, 2) + " %") : "") + 524 item("Final / cost", (final / cost).toFixed(2) + "\u00D7"); 525 } 526 ["roiCost", "roiFinal", "roiYears"].forEach(function (id) { on($(id), "input", calc); }); 527 calc(); 528 }; 529 530 /* ââââââââ PDF (pdf-lib) ââââââââ */ 531 function fileList(listEl, files, onChange) { 532 listEl.innerHTML = ""; 533 files.forEach(function (f, idx) { 534 var li = document.createElement("li"); 535 li.draggable = true; li.dataset.idx = idx; 536 li.innerHTML = '<span class="fl-drag">\u22EE\u22EE</span><span class="fl-name">' + f.name + 537 '</span><span class="fl-size">' + humanSize(f.size) + '</span><button class="fl-x" aria-label="Remove">\u00D7</button>'; 538 li.querySelector(".fl-x").addEventListener("click", function () { files.splice(idx, 1); onChange(); }); 539 li.addEventListener("dragstart", function () { li.classList.add("dragging"); listEl._drag = idx; }); 540 li.addEventListener("dragend", function () { li.classList.remove("dragging"); }); 541 li.addEventListener("dragover", function (e) { e.preventDefault(); }); 542 li.addEventListener("drop", function (e) { 543 e.preventDefault(); 544 var from = listEl._drag, to = idx; 545 if (from === to) return; 546 var moved = files.splice(from, 1)[0]; 547 files.splice(to, 0, moved); onChange(); 548 }); 549 listEl.appendChild(li); 550 }); 551 } 552 553 T.mergePdf = function (panel) { 554 var files = [], list = $("mpList"), st = $("mpStatus"); 555 function refresh() { fileList(list, files, refresh); $("mpActions").hidden = files.length === 0; } 556 wireDrop("mpDrop", "mpFile", function (fl) { 557 [].forEach.call(fl, function (f) { if (f.type === "application/pdf") files.push(f); }); 558 refresh(); 559 }); 560 panel.querySelectorAll("[data-act]").forEach(function (b) { 561 on(b, "click", function () { 562 var a = b.getAttribute("data-act"); 563 if (a === "clear") { files.length = 0; refresh(); st.textContent = ""; } 564 else if (a === "merge") merge(); 565 }); 566 }); 567 async function merge() { 568 if (!window.PDFLib) { st.textContent = "PDF library still loading, try again in a moment."; st.className = "tp-status err"; return; } 569 if (!files.length) return; 570 st.textContent = "Merging\u2026"; st.className = "tp-status"; 571 try { 572 var outDoc = await PDFLib.PDFDocument.create(); 573 for (var i = 0; i < files.length; i++) { 574 var bytes = await files[i].arrayBuffer(); 575 var src = await PDFLib.PDFDocument.load(bytes, { ignoreEncryption: true }); 576 var pages = await outDoc.copyPages(src, src.getPageIndices()); 577 pages.forEach(function (p) { outDoc.addPage(p); }); 578 } 579 downloadBlob(new Blob([await outDoc.save()], { type: "application/pdf" }), "merged.pdf"); 580 st.textContent = "\u2713 Merged " + files.length + " files"; st.className = "tp-status ok"; 581 } catch (e) { st.textContent = "\u2717 " + e.message; st.className = "tp-status err"; } 582 } 583 }; 584 585 T.imagesToPdf = function (panel) { 586 var files = [], list = $("i2pList"), st = $("i2pStatus"); 587 function refresh() { 588 fileList(list, files, refresh); 589 var has = files.length > 0; 590 $("i2pActions").hidden = !has; $("i2pControls").hidden = !has; 591 } 592 wireDrop("i2pDrop", "i2pFile", function (fl) { 593 [].forEach.call(fl, function (f) { if (/image\/(jpeg|png)/.test(f.type)) files.push(f); }); 594 refresh(); 595 }); 596 panel.querySelectorAll("[data-act]").forEach(function (b) { 597 on(b, "click", function () { 598 var a = b.getAttribute("data-act"); 599 if (a === "clear") { files.length = 0; refresh(); st.textContent = ""; } 600 else if (a === "convert") convert(); 601 }); 602 }); 603 async function convert() { 604 if (!window.PDFLib) { st.textContent = "PDF library still loading, try again."; st.className = "tp-status err"; return; } 605 if (!files.length) return; 606 st.textContent = "Building PDF\u2026"; st.className = "tp-status"; 607 var sizes = { a4: [595.28, 841.89], letter: [612, 792] }; 608 var mode = $("i2pSize").value;
609 try { 610 var doc = await PDFLib.PDFDocument.create(); 611 for (var i = 0; i < files.length; i++) { 612 var bytes = await files[i].arrayBuffer(); 613 var img = files[i].type === "image/png" ? await doc.embedPng(bytes) : await doc.embedJpg(bytes); 614 if (mode === "fit") { 615 var p1 = doc.addPage([img.width, img.height]); 616 p1.drawImage(img, { x: 0, y: 0, width: img.width, height: img.height }); 617 } else { 618 var s = sizes[mode], p2 = doc.addPage(s); 619 var scale = Math.min(s[0] / img.width, s[1] / img.height) * 0.92; 620 var w = img.width * scale, h = img.height * scale; 621 p2.drawImage(img, { x: (s[0] - w) / 2, y: (s[1] - h) / 2, width: w, height: h }); 622 } 623 } 624 downloadBlob(new Blob([await doc.save()], { type: "application/pdf" }), "images.pdf"); 625 st.textContent = "\u2713 Created PDF with " + files.length + " pages"; st.className = "tp-status ok"; 626 } catch (e) { st.textContent = "\u2717 " + e.message; st.className = "tp-status err"; } 627 } 628 }; 629 630 /* ââââââââ AI PRODUCTIVITY ââââââââ */ 631 T.tokenEstimator = function () { 632 var inp = $("tokInput"), grid = $("tokStats"), ctx = $("tokContext"); 633 function run() { 634 var t = inp.value; 635 var chars = t.length; 636 var words = (t.trim().match(/\S+/g) || []).length; 637 var byChar = chars / 4, byWord = words / 0.75; 638 var tokens = Math.round((byChar + byWord) / 2); 639 grid.innerHTML = stat(fmt(tokens, 0), "Tokens \u2248") + stat(fmt(words, 0), "Words") + stat(fmt(chars, 0), "Characters"); 640 if (!tokens) { ctx.textContent = ""; return; } 641 var windows = [["8K", 8192], ["128K", 128000], ["200K", 200000]]; 642 ctx.textContent = "Approx. context usage: " + windows.map(function (w) { return Math.round((tokens / w[1]) * 100) + "% of " + w[0]; }).join(" \u00B7 "); 643 } 644 on(inp, "input", run); run(); 645 }; 646 647 T.readabilityChecker = function () { 648 var inp = $("readInput"), out = $("readResult"), grid = $("readStats"); 649 function syllables(word) { 650 word = word.toLowerCase().replace(/[^a-z]/g, ""); 651 if (word.length <= 3) return word ? 1 : 0; 652 word = word.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, "").replace(/^y/, ""); 653 var m = word.match(/[aeiouy]{1,2}/g); 654 return m ? m.length : 1; 655 } 656 function run() { 657 var t = inp.value.trim(); 658 if (!t) { out.textContent = "\u2014"; grid.innerHTML = ""; return; } 659 var sentences = (t.match(/[.!?]+(\s|$)/g) || []).length || 1; 660 var words = t.match(/\S+/g) || []; 661 var wc = words.length || 1; 662 var syl = words.reduce(function (a, w) { return a + syllables(w); }, 0); 663 var wps = wc / sentences, spw = syl / wc; 664 var flesch = 206.835 - 1.015 * wps - 84.6 * spw; 665 var grade = 0.39 * wps + 11.8 * spw - 15.59; 666 flesch = Math.max(0, Math.min(100, flesch)); 667 var label = flesch >= 70 ? "Easy" : flesch >= 60 ? "Standard" : flesch >= 30 ? "Fairly hard" : "Very hard"; 668 out.innerHTML = fmt(flesch, 1) + "<small>Flesch Reading Ease \u00B7 " + label + "</small>"; 669 grid.innerHTML = 670 stat("~" + Math.max(1, Math.round(grade)), "Grade level") + 671 stat(fmt(wps, 1), "Words / sentence") + 672 stat(fmt(spw, 2), "Syllables / word") + 673 stat(fmt(wc, 0), "Words"); 674 } 675 on(inp, "input", run); run(); 676 }; 677 678 /* ââ small builders ââ */ 679 function item(label, value) { 680 return '<div class="bd-item"><div class="bd-label">' + label + '</div><div class="bd-value">' + value + "</div></div>"; 681 } 682 function stat(val, lab) { 683 return '<div class="stat"><div class="s-val">' + val + '</div><div class="s-lab">' + lab + "</div></div>"; 684 } 685 function downloadBlob(blob, name) { 686 var url = URL.createObjectURL(blob); 687 var a = document.createElement("a"); 688 a.href = url;
688 a.download = name; document.body.appendChild(a); a.click(); 689 setTimeout(function () { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1000); 690 } 691 692 /* âââââââââââ PHASE 2 â NEW TOOLS âââââââââââ */ 693 694 /* Tip calculator */ 695 T.tipCalculator = function (panel) { 696 var stats = $("tipStats"); 697 function calc() { 698 var bill = num("tipBill"), pct = num("tipPct"), split = Math.max(1, Math.floor(num("tipSplit"))); 699 var tip = bill * pct / 100, total = bill + tip; 700 stats.innerHTML = 701 stat(fmt(tip, 2), "Tip amount") + 702 stat(fmt(total, 2), "Total bill") + 703 stat(fmt(total / split, 2), split > 1 ? "Each of " + split : "Per person"); 704 } 705 panel.querySelectorAll("[data-tip]").forEach(function (b) { 706 on(b, "click", function () { 707 $("tipPct").value = b.getAttribute("data-tip"); 708 panel.querySelectorAll("[data-tip]").forEach(function (x) { x.classList.remove("btn-primary"); }); 709 b.classList.add("btn-primary"); 710 calc(); 711 }); 712 }); 713 panel.addEventListener("input", calc); 714 calc(); 715 }; 716 717 /* Unit converter */ 718 var UNITS = { 719 length: [["m", "Meters", 1], ["km", "Kilometers", 1000], ["cm", "Centimeters", 0.01], ["mm", "Millimeters", 0.001], ["mi", "Miles", 1609.344], ["yd", "Yards", 0.9144], ["ft", "Feet", 0.3048], ["in", "Inches", 0.0254]], 720 mass: [["kg", "Kilograms", 1000], ["g", "Grams", 1], ["mg", "Milligrams", 0.001], ["t", "Metric tons", 1e6], ["lb", "Pounds", 453.59237], ["oz", "Ounces", 28.349523125], ["st", "Stones", 6350.29318]], 721 temperature: [["c", "Celsius", 0], ["f", "Fahrenheit", 0], ["k", "Kelvin", 0]], 722 volume: [["l", "Liters", 1], ["ml", "Milliliters", 0.001], ["gal", "Gallons (US)", 3.785411784], ["qt", "Quarts (US)", 0.946352946], ["pt", "Pints (US)", 0.473176473], ["cup", "Cups (US)", 0.2365882365], ["floz", "Fluid oz (US)", 0.0295735295625]], 723 area: [["m2", "Sq meters", 1], ["km2", "Sq kilometers", 1e6], ["ft2", "Sq feet", 0.09290304], ["yd2", "Sq yards", 0.83612736], ["ha", "Hectares", 1e4], ["ac", "Acres", 4046.8564224]], 724 speed: [["ms", "m/s", 1], ["kmh", "km/h", 0.277777778], ["mph", "mph", 0.44704], ["kn", "Knots", 0.514444444], ["fts", "ft/s", 0.3048]], 725 data: [["b", "Bytes", 1], ["kb", "Kilobytes", 1024], ["mb", "Megabytes", 1048576], ["gb", "Gigabytes", 1073741824], ["tb", "Terabytes", 1099511627776]], 726 }; 727 T.unitConverter = function (panel) { 728 var cat = $("unitCat"), from = $("unitFrom"), to = $("unitTo"), out = $("unitResult"); 729 function opts(sel, list, pick) { 730 sel.innerHTML = list.map(function (u, i) { 731 return '<option value="' + u[0] + '"' + (i === pick ? " selected" : "") + ">" + u[1] + "</option>"; 732 }).join(""); 733 } 734 function fill() { 735 var list = UNITS[cat.value]; 736 opts(from, list, 0); 737 opts(to, list, Math.min(1, list.length - 1)); 738 } 739 function tempTo(v, u) { return u === "f" ? (v - 32) * 5 / 9 : u === "k" ? v - 273.15 : v; } 740 function tempFrom(c, u) { return u === "f" ? c * 9 / 5 + 32 : u === "k" ? c + 273.15 : c; } 741 function label(list, key) { for (var i = 0; i < list.length; i++) if (list[i][0] === key) return list[i][1]; return key; } 742 function calc() { 743 var list = UNITS[cat.value], v = num("unitVal"), res; 744 if (cat.value === "temperature") res = tempFrom(tempTo(v, from.value), to.value); 745 else { 746 var f = 1, t = 1; 747 list.forEach(function (u) { if (u[0] === from.value) f = u[2]; if (u[0] === to.value) t = u[2]; }); 748 res = v * f / t; 749 } 750 var dp = Math.abs(res) >= 1000 || res === 0 ? 2 : 6; 751 out.innerHTML = fmt(v, 2) + " " + label(list, from.value) + " = <strong>" + fmt(res, dp) + "</strong> " + label(list, to.value); 752 } 753 on(cat, "change", function () { fill(); calc(); }); 754 panel.addEventListener("input", calc); 755 panel.querySelectorAll("[data-act=swap]").forEach(function (b) { 756 on(b, "click", function () { var tmp = from.value; from.value = to.value; to.value = tmp; calc(); }); 757 }); 758 fill(); calc(); 759 }; 760 761 /* GPA calculator */ 762 var GRADES = [["A", 4.0], ["A-", 3.7], ["B+", 3.3], ["B", 3.0], ["B-", 2.7], ["C+", 2.3], ["C", 2.0], ["C-", 1.7], ["D+", 1.3], ["D", 1.0], ["F", 0.0]]; 763 T.gpaCalculator = function (panel) { 764 var rows = $("gpaRows"), out = $("gpaResult"), bd = $("gpaBreakdown"); 765 function addRow(name, gi, cr) { 766 var div = document.createElement("div"); 767 div.className = "gpa-row"; 768 div.innerHTML = 769 '<input type="text" class="gpa-name" placeholder="Course" value="' + (name || "") + '">' + 770 '<select class="select gpa-grade">' + GRADES.map(function (g, i) { 771 return '<option value="' + g[1] + '"' + (i === (gi == null ? 0 : gi) ? " selected" : "") + ">" + g[0] + "</option>"; 772 }).join("") + "</select>" + 773 '<input type="number" class="gpa-cr" min="0" step="0.5" value="' + (cr == null ? 3 : cr) + '">' + 774 '<button class="gpa-x" aria-label="Remove">Ã</button>'; 775 div.querySelector(".gpa-x").addEventListener("click", function () { div.remove();
775 calc(); }); 776 rows.appendChild(div); 777 } 778 function calc() { 779 var qp = 0, cr = 0; 780 rows.querySelectorAll(".gpa-row").forEach(function (r) { 781 var pts = parseFloat(r.querySelector(".gpa-grade").value); 782 var c = parseFloat(r.querySelector(".gpa-cr").value) || 0; 783 qp += pts * c; cr += c; 784 }); 785 if (!cr) { out.textContent = "â"; bd.innerHTML = ""; return; } 786 out.innerHTML = fmt(qp / cr, 2) + "<small>weighted GPA · 4.0 scale</small>"; 787 bd.innerHTML = item("Total credits", fmt(cr, 1)) + item("Quality points", fmt(qp, 1)) + item("Courses", rows.querySelectorAll(".gpa-row").length); 788 } 789 panel.querySelectorAll("[data-act=add]").forEach(function (b) { on(b, "click", function () { addRow(); calc(); }); }); 790 panel.addEventListener("input", calc); 791 panel.addEventListener("change", calc); 792 addRow("", 0, 3); addRow("", 2, 4); addRow("", 1, 3); 793 calc(); 794 }; 795 796 /* Sales tax calculator */ 797 T.salesTaxCalculator = function (panel) { 798 var out = $("stResult"), bd = $("stBreakdown"); 799 function calc() { 800 var tab = activeTab(panel); 801 if (tab === "add") { 802 var net = num("stAmount"), rate = num("stRate"), tax = net * rate / 100; 803 out.innerHTML = fmt(net + tax, 2) + "<small>total including " + fmt(rate, 2) + "% tax</small>"; 804 bd.innerHTML = item("Net amount", fmt(net, 2)) + item("Tax", fmt(tax, 2)) + item("Total", fmt(net + tax, 2)); 805 } else { 806 var gross = num("stGross"), r = num("stRate2"), netv = gross / (1 + r / 100), taxv = gross - netv; 807 out.innerHTML = fmt(netv, 2) + "<small>net, before " + fmt(r, 2) + "% tax</small>"; 808 bd.innerHTML = item("Gross (incl. tax)", fmt(gross, 2)) + item("Tax portion", fmt(taxv, 2)) + item("Net amount", fmt(netv, 2)); 809 } 810 } 811 panel.addEventListener("input", calc); 812 panel.addEventListener("tabchange", calc); 813 calc(); 814 }; 815 816 /* Mortgage calculator */ 817 T.mortgageCalculator = function (panel) { 818 var out = $("mResult"), bd = $("mBreakdown"); 819 function calc() { 820 var home = num("mHome"), down = num("mDown"), rate = num("mRate"), yrs = num("mTerm"); 821 var loan = Math.max(0, home - down), n = yrs * 12, r = rate / 100 / 12, pi; 822 if (r === 0) pi = n ? loan / n : 0; 823 else pi = loan * r * Math.pow(1 + r, n) / (Math.pow(1 + r, n) - 1); 824 var taxM = num("mTax") / 12, insM = num("mIns") / 12; 825 var monthly = pi + taxM + insM; 826 if (!isFinite(monthly) || monthly <= 0) { out.textContent = "â"; bd.innerHTML = ""; return; } 827 var totalInterest = pi * n - loan; 828 out.innerHTML = fmt(monthly, 2) + "<small>estimated monthly payment</small>"; 829 bd.innerHTML = 830 item("Loan amount", fmt(loan, 2)) + 831 item("Principal & interest", fmt(pi, 2)) + 832 (taxM ? item("Property tax /mo", fmt(taxM, 2)) : "") + 833 (insM ? item("Insurance /mo", fmt(insM, 2)) : "") + 834 item("Total interest", fmt(totalInterest, 2)); 835 } 836 panel.addEventListener("input", calc); 837 calc(); 838 }; 839 840 /* Investment calculator */ 841 T.investmentCalculator = function (panel) { 842 var out = $("invResult"), bd = $("invBreakdown"); 843 function calc() { 844 var P = num("invInit"), pmt = num("invMon"), r = num("invRate") / 100, yrs = num("invYears"); 845 var months = yrs * 12, mi = r / 12; 846 var fvP = P * Math.pow(1 + mi, months); 847 var fvC = mi === 0 ? pmt * months : pmt * ((Math.pow(1 + mi, months) - 1) / mi); 848 var total = fvP + fvC, contributed = P + pmt * months, growth = total - contributed; 849 if (!isFinite(total)) { out.textContent = "â"; bd.innerHTML = ""; return; } 850 out.innerHTML = fmt(total, 2) + "<small>projected value after " + yrs + " years</small>"; 851 bd.innerHTML = 852 item("Total contributed", fmt(contributed, 2)) + 853 item("Growth", fmt(growth, 2)) + 854 item("Growth share", (total ? Math.round(growth / total * 100) : 0) + "%"); 855 } 856 panel.addEventListener("input", calc); 857 calc(); 858 }; 859 860 /* Salary converter */ 861 T.salaryCalculator = function (panel) { 862 var stats = $("salStats"); 863 function calc() {
864 var amt = num("salAmount"), period = $("salPeriod").value, hpw = num("salHours") || 40, wpy = num("salWeeks") || 52; 865 var hoursPerYear = hpw * wpy; 866 var annual; 867 if (period === "hour") annual = amt * hoursPerYear; 868 else if (period === "day") annual = amt * (hpw / 5) * wpy; // assume 5-day week 869 else if (period === "week") annual = amt * wpy; 870 else if (period === "month") annual = amt * 12; 871 else annual = amt; 872 var hourly = hoursPerYear ? annual / hoursPerYear : 0; 873 stats.innerHTML = 874 stat(fmt(hourly, 2), "Hourly") + 875 stat(fmt(annual / wpy, 2), "Weekly") + 876 stat(fmt(annual / 12, 2), "Monthly") + 877 stat(fmt(annual, 2), "Annual"); 878 } 879 panel.addEventListener("input", calc); 880 panel.addEventListener("change", calc); 881 calc(); 882 }; 883 884 /* Password generator */ 885 T.passwordGenerator = function (panel) { 886 var out = $("pgOut"), bar = $("pgStrength"), lenVal = $("pgLenVal"); 887 var SETS = { 888 pgUpper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 889 pgLower: "abcdefghijklmnopqrstuvwxyz", 890 pgNum: "0123456789", 891 pgSym: "!@#$%^&*()-_=+[]{};:,.<>?", 892 }; 893 var AMBIG = /[l1IO0]/g; 894 function gen() { 895 var pool = ""; 896 ["pgUpper", "pgLower", "pgNum", "pgSym"].forEach(function (id) { if ($(id).checked) pool += SETS[id]; }); 897 if ($("pgAmbig").checked) pool = pool.replace(AMBIG, ""); 898 if (!pool) { out.value = ""; strength(0, 0); return; } 899 var len = parseInt($("pgLen").value, 10); 900 var bytes = crypto.getRandomValues(new Uint32Array(len)); 901 var pw = ""; 902 for (var i = 0; i < len; i++) pw += pool[bytes[i] % pool.length]; 903 out.value = pw; 904 strength(len, pool.length); 905 } 906 function strength(len, poolSize) { 907 var bits = len && poolSize ? len * Math.log2(poolSize) : 0; 908 var pct = Math.max(0, Math.min(100, Math.round(bits / 128 * 100))); 909 var color = bits < 40 ? "var(--err)" : bits < 70 ? "var(--warn)" : "var(--ok)"; 910 bar.style.width = pct + "%"; 911 bar.style.background = color; 912 } 913 on($("pgLen"), "input", function () { lenVal.textContent = $("pgLen").value; gen(); }); 914 panel.querySelectorAll("input[type=checkbox]").forEach(function (c) { on(c, "change", gen); }); 915 panel.querySelectorAll("[data-act]").forEach(function (b) { 916 on(b, "click", function () { 917 var a = b.getAttribute("data-act"); 918 if (a === "generate") gen(); 919 else if (a === "copy") { if (out.value) copy(out.value, "Password copied"); } 920 }); 921 }); 922 gen(); 923 }; 924 925 /* QR code generator */ 926 T.qrCodeGenerator = function (panel) { 927 var stage = $("qrStage"); 928 function gen() { 929 if (!window.QRCode) { stage.innerHTML = '<p class="tp-status err">QR library still loading â try again.</p>'; return; } 930 var text = $("qrText").value.trim(); 931 if (!text) { stage.innerHTML = '<p class="tp-note">Enter something to encode.</p>'; return; } 932 stage.innerHTML = ""; 933 var size = parseInt($("qrSize").value, 10); 934 var lvl = { L: 1, M: 0, Q: 3, H: 2 }[$("qrEc").value]; // QRCode.CorrectLevel: M=0,L=1,H=2,Q=3 935 new window.QRCode(stage, { text: text, width: size, height: size, correctLevel: lvl }); 936 } 937 function download() { 938 var c = stage.querySelector("canvas"), url; 939 if (c) url = c.toDataURL("image/png"); 940 else { var im = stage.querySelector("img"); if (!im) return; url = im.src; } 941 var a = document.createElement("a"); a.href = url; a.download = "qr-code.png"; 942 document.body.appendChild(a); a.click(); document.body.removeChild(a); 943 } 944 panel.querySelectorAll("[data-act]").forEach(function (b) { 945 on(b, "click", function () { b.getAttribute("data-act") === "download" ? download() : gen(); }); 946 }); 947 on($("qrText"), "input", gen); 948 on($("qrSize"), "change", gen); 949 on($("qrEc"), "change", gen); 950 gen(); 951 }; 952 953 /* Color converter */ 954 T.colorConverter = function (panel) { 955 var picker = $("ccPicker"), hex = $("ccHex"), swatch = $("ccSwatch"), out = $("ccOut"); 956 function norm(h) { 957 h = h.replace(/^#/, "").trim(); 958 if (/^[0-9a-f]{3}$/i.test(h)) h = h.split("").map(function (c) { return c + c; }).join(""); 959 return /^[0-9a-f]{6}$/i.test(h) ? "#" + h.toLowerCase() : null; 960 } 961 function toRgb(h) { return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)]; } 962 function toHsl(r, g, b) { 963 r /= 255; g /= 255; b /= 255; 964 var mx = Math.max(r, g, b), mn = Math.min(r, g, b), h, s, l = (mx + mn) / 2; 965 if (mx === mn) { h = s = 0; } 966 else { 967 var d = mx - mn; 968 s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn); 969 if (mx === r) h = (g - b) / d + (g < b ? 6 : 0); 970 else if (mx === g) h = (b - r) / d + 2; 971 else h = (r - g) / d + 4; 972 h /= 6; 973 } 974 return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; 975 } 976 function row(label, val) { 977 return '<div class="cc-line"><span class="cc-label">' + label + '</span><code class="cc-val">' + val + 978 '</code><button class="btn cc-copy" data-copy="' + val.replace(/"/g, """) + '">Copy</button></div>'; 979 } 980 function render(h) { 981 var rgb = toRgb(h), hsl = toHsl(rgb[0], rgb[1], rgb[2]); 982 swatch.style.background = h; 983 var rgbStr = "rgb(" + rgb.join(", ") + ")", hslStr = "hsl(" + hsl[0] + ", " + hsl[1] + "%, " + hsl[2] + "%)"; 984 out.innerHTML = row("HEX", h) + row("RGB", rgbStr) + row("HSL", hslStr); 985 out.querySelectorAll(".cc-copy").forEach(function (b) { 986 on(b, "click", function () { copy(b.getAttribute("data-copy"), "Copied " + b.getAttribute("data-copy")); }); 987 }); 988 } 989 on(picker, "input", function () { hex.value = picker.value; render(picker.value); }); 990 on(hex, "input", function () { var n = norm(hex.value); if (n) { picker.value = n; render(n); } }); 991 render(picker.value); 992 }; 993 994 /* Text diff (line-based LCS) */ 995 T.textDiff = function (panel) { 996 var out = $("diffOut"); 997 function diff(a, b) { 998 var m = a.length, n = b.length, dp = []; 999 for (var i = 0; i <= m; i++) { dp[i] = []; for (var j = 0; j <= n; j++) dp[i][j] = 0; } 1000 for (i = m - 1; i >= 0; i--) for (j = n - 1; j >= 0; j--) 1001 dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]); 1002 var res = []; i = 0; j = 0; 1003 while (i < m && j < n) { 1004 if (a[i] === b[j]) { res.push(["=", a[i]]); i++; j++; } 1005 else if (dp[i + 1][j] >= dp[i][j + 1]) { res.push(["-", a[i]]); i++; } 1006 else { res.push(["+", b[j]]); j++; } 1007 } 1008 while (i < m) { res.push(["-", a[i]]); i++; } 1009 while (j < n) { res.push(["+", b[j]]); j++; } 1010 return res; 1011 } 1012 function run() { 1013 var a = $("diffA").value.split("\n"), b = $("diffB").value.split("\n"); 1014 var rows = diff(a, b), adds = 0, dels = 0; 1015 var html = rows.map(function (r) { 1016 var cls = r[0] === "+" ? "d-add" : r[0] === "-" ? "d-del" : "d-eq"; 1017 if (r[0] === "+") adds++; if (r[0] === "-") dels++; 1018 var sign = r[0] === "=" ? " " : r[0]; 1019 return '<div class="' + cls + '"><span class="d-sign">' + sign + "</span>" + (esc(r[1]) || " ") + "</div>"; 1020 }).join(""); 1021 out.innerHTML = '<div class="d-summary"><span class="d-add-c">+' + adds + " added</span><span class=\"d-del-c\">-" + dels + " removed</span></div>" + html; 1022 } 1023 function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { "&": "&", "<": "<", ">": ">" }[c]; }); } 1024 panel.querySelectorAll("[data-act=compare]").forEach(function (b) { on(b, "click", run); }); 1025 run(); 1026 }; 1027 1028 /* Character counter */ 1029 T.characterCounter = function () { 1030 var inp = $("chInput"), grid = $("chStats"), bars = $("chLimits"); 1031 var LIMITS = [["X / Twitter post", 280], ["SMS message", 160], ["Meta description", 160], ["Title tag", 60]]; 1032 function run() { 1033 var t = inp.value, chars = t.length, noSpace = t.replace(/\s/g, "").length; 1034 var words = (t.trim().match(/\S+/g) || []).length, lines = t ? t.split("\n").length : 0; 1035 grid.innerHTML = stat(fmt(chars, 0), "Characters") + stat(fmt(noSpace, 0), "No spaces") + stat(fmt(words, 0), "Words") + stat(fmt(lines, 0
1035), "Lines"); 1036 bars.innerHTML = LIMITS.map(function (l) { 1037 var pct = Math.min(100, chars / l[1] * 100), over = chars > l[1]; 1038 return '<div class="limit-bar' + (over ? " over" : "") + '"><div class="lb-head"><span>' + l[0] + 1039 '</span><span>' + chars + " / " + l[1] + "</span></div><div class=\"lb-track\"><div class=\"lb-fill\" style=\"width:" + pct + "%\"></div></div></div>"; 1040 }).join(""); 1041 } 1042 on(inp, "input", run); run(); 1043 }; 1044 1045 /* Image cropper */ 1046 T.imageCropper = function (panel) { 1047 var img = $("cropImg"), stage = $("cropStage"), box = $("cropBox"), ratio = null, natW = 0, natH = 0; 1048 wireDrop("cropDrop", "cropFile", function (files) { 1049 loadImage(files[0]).then(function (im) { 1050 natW = im.naturalWidth; natH = im.naturalHeight; 1051 img.src = im.src; 1052 $("cropControls").hidden = false; 1053 setTimeout(resetBox, 30); 1054 }); 1055 }); 1056 function stageRect() { return img.getBoundingClientRect(); } 1057 function resetBox() { 1058 var r = stageRect(), w = r.width * 0.6, h = ratio ? w / ratio : r.height * 0.6; 1059 if (h > r.height) { h = r.height * 0.9; w = ratio ? h * ratio : w; } 1060 place((r.width - w) / 2, (r.height - h) / 2, w, h); 1061 } 1062 function place(x, y, w, h) {
1063 var r = stageRect(); 1064 w = Math.max(24, Math.min(w, r.width)); h = Math.max(24, Math.min(h, r.height)); 1065 x = Math.max(0, Math.min(x, r.width - w)); y = Math.max(0, Math.min(y, r.height - h)); 1066 box.style.left = x + "px"; box.style.top = y + "px"; box.style.width = w + "px"; box.style.height = h + "px"; 1067 } 1068 function getBox() { return { x: parseFloat(box.style.left), y: parseFloat(box.style.top), w: parseFloat(box.style.width), h: parseFloat(box.style.height) }; } 1069 // drag to move 1070 var drag = null; 1071 box.addEventListener("pointerdown", function (e) { 1072 if (e.target.classList.contains("crop-handle")) return; 1073 var b = getBox(); drag = { sx: e.clientX, sy: e.clientY, bx: b.x, by: b.y }; box.setPointerCapture(e.pointerId); 1074 }); 1075 box.addEventListener("pointermove", function (e) { 1076 if (!drag) return; 1077 place(drag.bx + (e.clientX - drag.sx), drag.by + (e.clientY - drag.sy), getBox().w, getBox().h); 1078 }); 1079 box.addEventListener("pointerup", function () { drag = null; }); 1080 // resize via br handle 1081 var rz = null, handle = box.querySelector(".crop-handle"); 1082 handle.addEventListener("pointerdown", function (e) { 1083 e.stopPropagation(); var b = getBox(); rz = { sx: e.clientX, sy: e.clientY, w: b.w, h: b.h }; handle.setPointerCapture(e.pointerId); 1084 }); 1085 handle.addEventListener("pointermove", function (e) { 1086 if (!rz) return; var b = getBox(); 1087 var w = rz.w + (e.clientX - rz.sx), h = ratio ? w / ratio : rz.h + (e.clientY - rz.sy); 1088 place(b.x, b.y, w, h); 1089 }); 1090 handle.addEventListener("pointerup", function () { rz = null; }); 1091 panel.querySelectorAll("[data-ratio]").forEach(function (b) { 1092 on(b, "click", function () { 1093 panel.querySelectorAll("[data-ratio]").forEach(function (x) { x.classList.remove("btn-primary"); }); 1094 b.classList.add("btn-primary"); 1095 var v = b.getAttribute("data-ratio"); 1096 ratio = v === "free" ? null : parseFloat(v); 1097 resetBox(); 1098 }); 1099 }); 1100 panel.querySelectorAll("[data-act=crop]").forEach(function (b) { 1101 on(b, "click", function () {
1102 var r = stageRect(), b2 = getBox(), scale = natW / r.width; 1103 var sx = b2.x * scale, sy = b2.y * scale, sw = b2.w * scale, sh = b2.h * scale; 1104 var c = document.createElement("canvas"); c.width = Math.round(sw); c.height = Math.round(sh); 1105 var im = new Image(); 1106 im.onload = function () { 1107 c.getContext("2d").drawImage(im, sx, sy, sw, sh, 0, 0, sw, sh); 1108 c.toBlob(function (blob) { if (blob) downloadBlob(blob, "cropped.png"); }, "image/png"); 1109 }; 1110 im.src = img.src; 1111 }); 1112 }); 1113 window.addEventListener("resize", function () { if (!$("cropControls").hidden) resetBox(); }); 1114 }; 1115 1116 /* Image format converter */ 1117 T.imageFormatConverter = function (panel) { 1118 var loaded = null; 1119 wireDrop("fmtDrop", "fmtFile", function (files) { 1120 loadImage(files[0]).then(function (im) { loaded = im; $("fmtControls").hidden = false; render(); }); 1121 }); 1122 function render() { 1123 if (!loaded) return; 1124 var type = $("fmtFormat").value, q = parseFloat($("fmtQuality").value); 1125 var c = document.createElement("canvas"); c.width = loaded.naturalWidth; c.height = loaded.naturalHeight; 1126 var ctx = c.getContext("2d"); 1127 if (type === "image/jpeg") { ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, c.width, c.height); } 1128 ctx.drawImage(loaded, 0, 0); 1129 c.toBlob(function (blob) { 1130 if (!blob) return; 1131 var url = URL.createObjectURL(blob), ext = type.split("/")[1] === "jpeg" ? "jpg" : type.split("/")[1]; 1132 $("fmtPreview").innerHTML = '<figure><img src="' + url + '"><figcaption>' + type.split("/")[1].toUpperCase() + " · " + humanSize(blob.size) + "</figcaption></figure>"; 1133 $("fmtPreview")._blob = blob; $("fmtPreview")._ext = ext; 1134 }, type, q); 1135 } 1136 on($("fmtFormat"), "change", render); 1137 on($("fmtQuality"), "input", render); 1138 panel.querySelectorAll("[data-act=download]").forEach(function (b) { 1139 on(b, "click", function () { 1140 var p = $("fmtPreview"); if (p._blob) downloadBlob(p._blob, "converted." + p._ext); 1141 }); 1142 }); 1143 }; 1144})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.