{"id":352,"date":"2025-08-31T04:55:50","date_gmt":"2025-08-31T04:55:50","guid":{"rendered":"https:\/\/bioskinetics.com.au\/?page_id=352"},"modified":"2025-08-31T05:16:05","modified_gmt":"2025-08-31T05:16:05","slug":"contract-highlighter","status":"publish","type":"page","link":"https:\/\/bioskinetics.com.au\/?page_id=352","title":{"rendered":"Contract Highlighter"},"content":{"rendered":"\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" \/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n<title>Contract Highlighter \u2013 Obligations, Dates &#038; Monetary Amounts<\/title>\n\n<!-- PDF.js (for PDF text extraction, client-side only) -->\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.min.js\" defer><\/script>\n<!-- Mammoth (for DOCX -> text, client-side only) -->\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/mammoth\/1.4.21\/mammoth.browser.min.js\" defer><\/script>\n\n<style>\n  :root{\n    --bg:#0b0f14;\n    --panel:#121821;\n    --ink:#e9eef6;\n    --muted:#a9b4c5;\n    --accent:#4fb3ff;\n    --accent-2:#a0e075;\n    --warn:#ffcf66;\n    --error:#ff7785;\n\n    --obligation:#fdefa4;\n    --date:#b7f0ff;\n    --money:#d1ffd6;\n\n    --hl-ink:#101418;\n    --ring: 0 0 0 2px rgba(79,179,255,.35);\n    --radius:18px;\n  }\n  *{box-sizing:border-box}\n  html,body{margin:0;padding:0;background:linear-gradient(180deg,#0b0f14 0%, #0e141d 100%);color:var(--ink);font:16px\/1.55 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}\n  a{color:var(--accent)}\n  h1,h2,h3{line-height:1.2;margin:0 0 .4rem}\n  h1{font-size:clamp(1.35rem,1.1rem + 1.5vw,2rem);letter-spacing:.2px}\n  h2{font-size:1rem;color:var(--muted);font-weight:600}\n  p{margin:.3rem 0}\n  small{color:var(--muted)}\n  .wrap{max-width:1100px;margin:28px auto;padding:0 16px}\n  .card{\n    background:linear-gradient(180deg,#0f1622,#0d141d);\n    border:1px solid rgba(255,255,255,.06);\n    border-radius:var(--radius);\n    box-shadow:0 8px 40px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.03);\n  }\n  .header{\n    display:flex;gap:14px;align-items:center;justify-content:space-between;padding:18px 20px;border-bottom:1px solid rgba(255,255,255,.06)\n  }\n  .header .title{display:flex;gap:12px;align-items:center}\n  .logo{\n    width:40px;height:40px;border-radius:12px;\n    background:\n      radial-gradient(100% 100% at 0% 0%, #4fb3ff 0%, transparent 50%),\n      radial-gradient(100% 100% at 100% 100%, #a0e075 0%, transparent 50%),\n      linear-gradient(135deg,#14202c 0%, #0e1621 100%);\n    box-shadow:inset 0 1px 0 rgba(255,255,255,.08), 0 6px 16px rgba(0,0,0,.4);\n  }\n  .controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center}\n  .button, button{\n    appearance:none;border:0;border-radius:14px;padding:10px 14px;cursor:pointer;\n    background:#162232;color:var(--ink);font-weight:600;letter-spacing:.2px;\n    border:1px solid rgba(255,255,255,.08);\n    box-shadow:0 3px 14px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.04);\n  }\n  .button:hover{filter:brightness(1.06)}\n  .button.primary{background:linear-gradient(180deg,#1f7cd6,#1765b2);border-color:#2a7fd4}\n  .button.ghost{background:transparent;border-color:rgba(255,255,255,.14)}\n  .button.warn{background:linear-gradient(180deg,#a06e0f,#7f5607);border-color:#bf8b2b}\n  input[type=\"file\"]{display:none}\n  label.file{\n    display:inline-flex;gap:10px;align-items:center;padding:10px 14px;border-radius:14px;\n    background:#162232;border:1px dashed rgba(255,255,255,.2);cursor:pointer;\n  }\n  .body{display:grid;grid-template-columns:300px 1fr;gap:18px;padding:18px}\n  @media (max-width:900px){.body{grid-template-columns:1fr}}\n  .panel{\n    padding:14px;border:1px solid rgba(255,255,255,.06);border-radius:14px;background:#0e1520\n  }\n  .panel h3{font-size:.95rem;margin-bottom:8px}\n  .row{display:flex;align-items:center;justify-content:space-between;margin:8px 0;color:var(--muted)}\n  .stat{display:flex;gap:8px;align-items:center;font-weight:600;color:var(--ink)}\n  .stat .dot{width:10px;height:10px;border-radius:999px}\n  .dot.obligation{background:var(--obligation)}\n  .dot.date{background:var(--date)}\n  .dot.money{background:var(--money)}\n\n  .viewer{\n    position:relative;min-height:320px;max-height:65vh;overflow:auto;padding:18px;\n    border:1px solid rgba(255,255,255,.08);border-radius:16px;background:#0c121a\n  }\n  .viewer.empty{display:grid;place-items:center;color:var(--muted);font-style:italic}\n  .viewer mark, .viewer .hl{padding:.15em .25em;border-radius:.45em}\n  .hl-obligation{background:var(--obligation);color:#231d00}\n  .hl-date{background:var(--date);color:#00151b}\n  .hl-money{background:var(--money);color:#00240b}\n\n  .filters{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0 6px}\n  .chip{\n    display:inline-flex;gap:8px;align-items:center;padding:6px 10px;border-radius:999px;\n    background:#0f1a28;border:1px solid rgba(255,255,255,.1);color:var(--ink);font-size:.9rem\n  }\n  .chip input{accent-color:var(--accent)}\n  .legend{display:flex;gap:12px;align-items:center;color:var(--muted);font-size:.85rem}\n  .legend span{display:inline-flex;gap:6px;align-items:center}\n  .legend .sw{width:14px;height:10px;border-radius:3px}\n  .sw.obligation{background:var(--obligation)}\n  .sw.date{background:var(--date)}\n  .sw.money{background:var(--money)}\n\n  .findings{margin-top:10px;max-height:220px;overflow:auto;padding-right:6px}\n  .findings details{border:1px solid rgba(255,255,255,.08);border-radius:10px;margin:8px 0;background:#0b111a}\n  .findings summary{cursor:pointer;padding:8px 10px;color:var(--ink)}\n  .findings ol, .findings ul{margin:0 0 10px 1.2rem}\n  .findings li{margin:6px 0}\n  .muted{color:var(--muted)}\n  .footer{padding:16px 20px;border-top:1px solid rgba(255,255,255,.06)}\n  .fineprint{color:var(--muted);font-size:.78rem;line-height:1.35}\n  .badge{display:inline-block;font-size:.75rem;background:#0f1a28;border:1px solid rgba(255,255,255,.1);padding:6px 10px;border-radius:999px;color:var(--muted)}\n  .hidden{display:none !important}\n  .notice{padding:10px 12px;border-radius:12px;background:#0f1825;border:1px solid rgba(255,255,255,.1);color:var(--muted);margin:8px 0}\n  .error{color:var(--error)}\n  .success{color:var(--accent-2)}\n  .mono{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace}\n<\/style>\n<\/head>\n<body>\n  <div class=\"wrap card\" id=\"app\">\n    <div class=\"header\">\n      <div class=\"title\">\n        <div class=\"logo\" aria-hidden=\"true\"><\/div>\n        <div>\n          <h1>Contract Highlighter<\/h1>\n          <h2>Automatically highlight <strong>Obligations<\/strong>, <strong>Dates<\/strong>, and <strong>Monetary Amounts<\/strong><\/h2>\n        <\/div>\n      <\/div>\n      <div class=\"controls\">\n        <label class=\"file\" title=\"Upload a PDF, DOCX, or TXT contract\">\n          <input id=\"fileInput\" type=\"file\" accept=\".pdf,.docx,.txt\" \/>\n          <span>\ud83d\udcc4 Choose Contract<\/span>\n        <\/label>\n        <button class=\"button ghost\" id=\"pasteToggle\" type=\"button\">\u270d\ufe0f Paste Text<\/button>\n        <button class=\"button\" id=\"exportHtml\" type=\"button\" disabled>\u2b07\ufe0f Download Highlighted<\/button>\n        <button class=\"button warn\" id=\"resetBtn\" type=\"button\">\u267b\ufe0f Reset<\/button>\n      <\/div>\n    <\/div>\n\n    <div class=\"body\">\n      <!-- Left: Sidebar \/ Findings -->\n      <aside class=\"panel\" id=\"sidebar\">\n        <h3>Findings<\/h3>\n\n        <div class=\"row\">\n          <span class=\"stat\"><span class=\"dot obligation\"><\/span> Obligations<\/span>\n          <span id=\"countObl\" class=\"badge\">0<\/span>\n        <\/div>\n        <div class=\"row\">\n          <span class=\"stat\"><span class=\"dot date\"><\/span> Dates<\/span>\n          <span id=\"countDates\" class=\"badge\">0<\/span>\n        <\/div>\n        <div class=\"row\">\n          <span class=\"stat\"><span class=\"dot money\"><\/span> Monetary<\/span>\n          <span id=\"countMoney\" class=\"badge\">0<\/span>\n        <\/div>\n\n        <div class=\"filters\">\n          <label class=\"chip\"><input type=\"checkbox\" id=\"filterObl\" checked \/> Show obligations<\/label>\n          <label class=\"chip\"><input type=\"checkbox\" id=\"filterDates\" checked \/> Show dates<\/label>\n          <label class=\"chip\"><input type=\"checkbox\" id=\"filterMoney\" checked \/> Show monetary<\/label>\n        <\/div>\n        <div class=\"legend\">\n          <span><span class=\"sw obligation\"><\/span> Obligation<\/span>\n          <span><span class=\"sw date\"><\/span> Date<\/span>\n          <span><span class=\"sw money\"><\/span> Money<\/span>\n        <\/div>\n\n        <div class=\"findings\" id=\"findings\">\n          <details open>\n            <summary>Obligations (sentences)<\/summary>\n            <ol id=\"listObl\"><\/ol>\n          <\/details>\n          <details open>\n            <summary>Dates (matches)<\/summary>\n            <ol id=\"listDates\"><\/ol>\n          <\/details>\n          <details open>\n            <summary>Monetary amounts (matches)<\/summary>\n            <ol id=\"listMoney\"><\/ol>\n          <\/details>\n        <\/div>\n\n        <div class=\"notice mono\" id=\"status\">Ready. Analysis runs in your browser\u2014files are not uploaded.<\/div>\n        <div class=\"controls\" style=\"margin-top:8px\">\n          <button class=\"button ghost\" id=\"copySummary\" type=\"button\" disabled>\ud83d\udccb Copy Summary<\/button>\n          <button class=\"button ghost\" id=\"exportCSV\" type=\"button\" disabled>\ud83e\uddfe Export CSV<\/button>\n        <\/div>\n      <\/aside>\n\n      <!-- Right: Viewer -->\n      <main class=\"panel\">\n        <div id=\"pasteArea\" class=\"notice hidden\">\n          <p><strong>Paste contract text below<\/strong> and click <em>Highlight<\/em>.<\/p>\n          <textarea id=\"pasteInput\" rows=\"8\" style=\"width:100%;border-radius:12px;border:1px solid rgba(255,255,255,.12);background:#0b111a;color:var(--ink);padding:10px\"><\/textarea>\n          <div class=\"controls\" style=\"margin-top:8px\">\n            <button class=\"button primary\" id=\"runOnPaste\" type=\"button\">\u2728 Highlight<\/button>\n          <\/div>\n        <\/div>\n\n        <div id=\"viewer\" class=\"viewer empty\" aria-live=\"polite\">\n          <div>Upload a contract (PDF\/DOCX\/TXT) or paste text to begin.<\/div>\n        <\/div>\n      <\/main>\n    <\/div>\n\n    <div class=\"footer\">\n      <div class=\"fineprint\">\n        <p><strong>Disclaimer<\/strong> (fine print): This tool uses pattern-based heuristics to surface possible obligations, dates, and monetary amounts. It may miss items or highlight incorrectly. It does not provide legal advice, opinion, or a substitute for professional review. You agree that use is at your own risk and that the site and its authors are not responsible for errors, omissions, or any losses arising from use. Always consult a qualified lawyer before relying on results.<\/p>\n        <p class=\"muted\">Privacy note: Processing happens locally in your browser. Uploaded files are not sent to a server. Third-party libraries (PDF.js &#038; Mammoth) load from a public CDN to enable client-side parsing.<\/p>\n      <\/div>\n    <\/div>\n  <\/div>\n\n<script>\n(function(){\n  \/\/ Wait for external scripts to be ready before enabling features\n  const ready = () => typeof window.pdfjsLib !== \"undefined\" && typeof window.mammoth !== \"undefined\";\n\n  const state = {\n    rawText: \"\",\n    highlightedHTML: \"\",\n    findings: { obligations: [], dates: [], money: [] },\n    filename: null\n  };\n\n  \/\/ Elements\n  const fileInput = document.getElementById('fileInput');\n  const viewer = document.getElementById('viewer');\n  const countObl = document.getElementById('countObl');\n  const countDates = document.getElementById('countDates');\n  const countMoney = document.getElementById('countMoney');\n  const listObl = document.getElementById('listObl');\n  const listDates = document.getElementById('listDates');\n  const listMoney = document.getElementById('listMoney');\n  const filterObl = document.getElementById('filterObl');\n  const filterDates = document.getElementById('filterDates');\n  const filterMoney = document.getElementById('filterMoney');\n  const exportHtml = document.getElementById('exportHtml');\n  const exportCSV = document.getElementById('exportCSV');\n  const copySummary = document.getElementById('copySummary');\n  const pasteToggle = document.getElementById('pasteToggle');\n  const pasteArea = document.getElementById('pasteArea');\n  const pasteInput = document.getElementById('pasteInput');\n  const runOnPaste = document.getElementById('runOnPaste');\n  const resetBtn = document.getElementById('resetBtn');\n  const statusBox = document.getElementById('status');\n\n  \/\/ Configure PDF.js worker\n  const ensurePDFWorker = () => {\n    if (window.pdfjsLib && (!window.pdfjsLib.GlobalWorkerOptions.workerSrc)) {\n      window.pdfjsLib.GlobalWorkerOptions.workerSrc =\n        \"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/3.11.174\/pdf.worker.min.js\";\n    }\n  };\n\n  \/\/ Utils\n  const escapeHtml = (str) => str\n    .replace(\/&\/g,\"&amp;\")\n    .replace(\/<\/g,\"&lt;\")\n    .replace(\/>\/g,\"&gt;\")\n    .replace(\/\"\/g,\"&quot;\")\n    .replace(\/'\/g,\"&#039;\");\n\n  const download = (filename, mime, content) => {\n    const blob = new Blob([content], {type: mime});\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.style.display='none'; a.href = url; a.download = filename;\n    document.body.appendChild(a); a.click();\n    setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); }, 0);\n  };\n\n  const setStatus = (msg, kind='') => {\n    statusBox.textContent = msg;\n    statusBox.className = 'notice mono';\n    if (kind==='error') statusBox.classList.add('error');\n    if (kind==='success') statusBox.classList.add('success');\n  };\n\n  \/\/ Regex patterns\n  const month = \"(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sept?(?:ember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\";\n  const ordinal = \"(?:st|nd|rd|th)?\";\n  const dateWord = `\\\\b${month}\\\\s+\\\\d{1,2}${ordinal}(?:,\\\\s*\\\\d{4})?\\\\b`;\n  const dateNumericDMY = \"\\\\b\\\\d{1,2}[\\\\\/.\\\\-]\\\\d{1,2}[\\\\\/.\\\\-]\\\\d{2,4}\\\\b\";\n  const dateISO = \"\\\\b\\\\d{4}[\\\\\/.\\\\-]\\\\d{1,2}[\\\\\/.\\\\-]\\\\d{1,2}\\\\b\";\n  const dateRelative = \"\\\\b(?:within|no\\\\s+later\\\\s+than|on\\\\s+or\\\\s+before)\\\\s+\\\\d+\\\\s+(?:business\\\\s+)?(?:day|days|week|weeks|month|months|year|years)\\\\b\";\n  const dateQuarter = \"\\\\bQ[1-4]\\\\s+\\\\d{4}\\\\b\";\n  const datePattern = new RegExp([dateWord, dateNumericDMY, dateISO, dateRelative, dateQuarter].join(\"|\"), \"gi\");\n\n  const moneyPattern = new RegExp([\n    \"(?:A\\\\$|US\\\\$|C\\\\$|\u20ac|\u00a3|\u00a5|\u20b9|\\\\$)\\\\s?\\\\d{1,3}(?:,\\\\d{3})*(?:\\\\.\\\\d{2})?(?:\\\\s?(?:million|billion|thousand|k|m|bn))?\",\n    \"(?:AUD|USD|GBP|EUR|JPY|CNY|INR|NZD|CAD|CHF|SGD|HKD)\\\\s?\\\\d+(?:,\\\\d{3})*(?:\\\\.\\\\d{2})?(?:\\\\s?(?:million|billion|thousand|k|m|bn))?\"\n  ].join(\"|\"), \"gi\");\n\n  const obligationKeywords = [\n    \"shall\", \"must\", \"agrees?\\\\s+to\", \"is\\\\s+required\\\\s+to\", \"are\\\\s+required\\\\s+to\",\n    \"responsible\\\\s+for\", \"obligated\\\\s+to\", \"undertakes?\\\\s+to\", \"will\\\\s+ensure\",\n    \"shall\\\\s+not\", \"must\\\\s+not\", \"will\\\\s+not\", \"comply\", \"perform\", \"maintain\",\n    \"notify\", \"indemnif(?:y|ies)\", \"warrant(?:s)?\", \"represent(?:s)?\\\\s+that\",\n    \"deliver\", \"provide\", \"pay\"\n  ];\n  const obligationPattern = new RegExp(\"\\\\b(\" + obligationKeywords.join(\"|\") + \")\\\\b\", \"i\");\n\n  const sentenceSplit = (text) => {\n    \/\/ Split on ., ?, !, ;, or double newlines while keeping indices\n    const parts = [];\n    let start = 0;\n    const regex = \/([.;!?])(\\s+|\\n+)|\\n{2,}\/g;\n    let m;\n    while ((m = regex.exec(text)) !== null) {\n      const end = m.index + (m[1] ? m[1].length : 0);\n      if (end > start) parts.push({ start, end, sentence: text.slice(start, end) });\n      start = regex.lastIndex;\n    }\n    if (start < text.length) parts.push({ start, end: text.length, sentence: text.slice(start) });\n    return parts;\n  };\n\n  function analyzeAndHighlight(raw){\n    state.rawText = raw;\n    const findings = { obligations: [], dates: [], money: [] };\n\n    const sentences = sentenceSplit(raw);\n    const segments = sentences.map(s => {\n      const isObl = obligationPattern.test(s.sentence);\n      if (isObl) findings.obligations.push(s.sentence.trim());\n      return { ...s, isObl };\n    });\n\n    const uniqueDates = new Set();\n    const uniqueMoney = new Set();\n\n    const highlightInline = (txt) => {\n      \/\/ escape HTML first\n      let safe = escapeHtml(txt);\n\n      \/\/ money first (avoids interfering with date commas)\n      safe = safe.replace(moneyPattern, (m) => {\n        uniqueMoney.add(m);\n        return `<mark class=\"hl hl-money\" title=\"Monetary\">${escapeHtml(m)}<\/mark>`;\n      });\n\n      \/\/ dates\n      safe = safe.replace(datePattern, (m) => {\n        uniqueDates.add(m);\n        return `<mark class=\"hl hl-date\" title=\"Date\">${escapeHtml(m)}<\/mark>`;\n      });\n\n      return safe;\n    };\n\n    \/\/ Build HTML with outer obligation wraps\n    const html = segments.map(seg => {\n      const inner = highlightInline(seg.sentence);\n      return seg.isObl\n        ? `<span class=\"hl hl-obligation\" title=\"Obligation\">${inner}<\/span>`\n        : inner;\n    }).join(\"\");\n\n    \/\/ Populate findings lists\n    findings.dates = Array.from(uniqueDates);\n    findings.money = Array.from(uniqueMoney);\n\n    state.findings = findings;\n    state.highlightedHTML = html;\n    render();\n  }\n\n  function render(){\n    \/\/ viewer\n    if (state.highlightedHTML && state.highlightedHTML.trim()){\n      viewer.classList.remove('empty');\n      viewer.innerHTML = state.highlightedHTML;\n      exportHtml.disabled = false;\n      exportCSV.disabled = false;\n      copySummary.disabled = false;\n    } else {\n      viewer.classList.add('empty');\n      viewer.innerHTML = `<div>Upload a contract (PDF\/DOCX\/TXT) or paste text to begin.<\/div>`;\n      exportHtml.disabled = true;\n      exportCSV.disabled = true;\n      copySummary.disabled = true;\n    }\n\n    \/\/ counts\n    countObl.textContent = state.findings.obligations.length;\n    countDates.textContent = state.findings.dates.length;\n    countMoney.textContent = state.findings.money.length;\n\n    \/\/ lists\n    listObl.innerHTML = \"\";\n    listDates.innerHTML = \"\";\n    listMoney.innerHTML = \"\";\n\n    state.findings.obligations.forEach((s, i) => {\n      const li = document.createElement('li');\n      li.innerHTML = `<span class=\"mono\">${escapeHtml(s)}<\/span>`;\n      listObl.appendChild(li);\n    });\n\n    const snippet = (needle) => {\n      const txt = state.rawText;\n      const idx = txt.toLowerCase().indexOf(needle.toLowerCase());\n      if (idx === -1) return needle;\n      const start = Math.max(0, idx - 40);\n      const end = Math.min(txt.length, idx + needle.length + 40);\n      return (start>0?'\u2026':'') + txt.slice(start, end).replace(\/\\n\/g,' ') + (end<txt.length?'\u2026':'');\n    };\n\n    state.findings.dates.forEach((d) => {\n      const li = document.createElement('li');\n      li.innerHTML = `<strong>${escapeHtml(d)}<\/strong><div class=\"muted mono\">${escapeHtml(snippet(d))}<\/div>`;\n      listDates.appendChild(li);\n    });\n    state.findings.money.forEach((m) => {\n      const li = document.createElement('li');\n      li.innerHTML = `<strong>${escapeHtml(m)}<\/strong><div class=\"muted mono\">${escapeHtml(snippet(m))}<\/div>`;\n      listMoney.appendChild(li);\n    });\n\n    \/\/ filters\n    const showObl = filterObl.checked;\n    const showDates = filterDates.checked;\n    const showMoney = filterMoney.checked;\n\n    viewer.querySelectorAll('.hl-obligation').forEach(el => el.style.background = showObl ? getComputedStyle(document.documentElement).getPropertyValue('--obligation') : 'transparent');\n    viewer.querySelectorAll('.hl-date').forEach(el => el.style.background = showDates ? getComputedStyle(document.documentElement).getPropertyValue('--date') : 'transparent');\n    viewer.querySelectorAll('.hl-money').forEach(el => el.style.background = showMoney ? getComputedStyle(document.documentElement).getPropertyValue('--money') : 'transparent');\n  }\n\n  \/\/ File handlers\n  async function readTXT(file){\n    const text = await file.text();\n    return text;\n  }\n\n  async function readDOCX(file){\n    const arrayBuffer = await file.arrayBuffer();\n    const res = await window.mammoth.extractRawText({arrayBuffer});\n    return res.value || \"\";\n  }\n\n  async function readPDF(file){\n    ensurePDFWorker();\n    const arrayBuffer = await file.arrayBuffer();\n    const pdf = await window.pdfjsLib.getDocument({data: arrayBuffer}).promise;\n    let fullText = \"\";\n    for (let i = 1; i <= pdf.numPages; i++){\n      const page = await pdf.getPage(i);\n      const c = await page.getTextContent();\n      const pageText = c.items.map(it => it.str).join(\" \").replace(\/\\s{2,}\/g,' ').trim();\n      fullText += pageText + \"\\n\\n\";\n      setStatus(`Parsing PDF\u2026 page ${i}\/${pdf.numPages}`);\n    }\n    return fullText.trim();\n  }\n\n  async function handleFile(file){\n    if (!file){ return; }\n    state.filename = file.name;\n    setStatus(`Reading \"${file.name}\"\u2026`);\n\n    const ext = file.name.toLowerCase().split('.').pop();\n    try {\n      let text = \"\";\n      if (ext === 'txt'){\n        text = await readTXT(file);\n      } else if (ext === 'docx'){\n        if (!window.mammoth) throw new Error(\"DOCX parser not loaded.\");\n        text = await readDOCX(file);\n      } else if (ext === 'pdf'){\n        if (!window.pdfjsLib) throw new Error(\"PDF parser not loaded.\");\n        text = await readPDF(file);\n      } else {\n        throw new Error(\"Unsupported file type. Please upload PDF, DOCX, or TXT.\");\n      }\n      if (!text || !text.trim()){\n        setStatus(\"No selectable text was found (the PDF may be scanned images). Try a text-based file.\", \"error\");\n        return;\n      }\n      setStatus(`Extracted ${text.length.toLocaleString()} characters. Highlighting\u2026`);\n      analyzeAndHighlight(text);\n      setStatus(`Done. Highlighted content from \"${file.name}\".`, \"success\");\n    } catch (e){\n      console.error(e);\n      setStatus(`Error: ${e.message}`, \"error\");\n    }\n  }\n\n  \/\/ Exports\n  function exportHighlightedHTML(){\n    const title = state.filename ? `Highlights \u2013 ${state.filename}` : 'Highlights';\n    const css = `\n    body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.6;padding:24px;max-width:900px;margin:0 auto;background:#fff;color:#111}\n    mark, .hl{padding:.15em .25em;border-radius:.35em}\n    .hl-obligation{background:#fdefa4}\n    .hl-date{background:#b7f0ff}\n    .hl-money{background:#d1ffd6}\n    `;\n    const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>${escapeHtml(title)}<\/title><style>${css}<\/style><\/head><body><h1>${escapeHtml(title)}<\/h1>${state.highlightedHTML}<\/body><\/html>`;\n    const baseName = (state.filename || 'contract').replace(\/\\.[^.]+$\/, '');\n    download(`${baseName}-highlighted.html`, 'text\/html;charset=utf-8', html);\n  }\n\n  function exportFindingsCSV(){\n    const rows = [[\"Type\",\"Value\",\"Context\"]];\n    const add = (type, arr) => arr.forEach(v => rows.push([type, v, (state.rawText||\"\").replace(\/\\n\/g,' ').slice(0,0)]));\n    \/\/ For CSV, include compact context snippets:\n    const ctx = (needle) => {\n      const t = state.rawText || \"\";\n      const idx = t.toLowerCase().indexOf(needle.toLowerCase());\n      if (idx === -1) return \"\";\n      const s = Math.max(0, idx - 40), e = Math.min(t.length, idx + needle.length + 40);\n      return t.slice(s, e).replace(\/\\n\/g,' ');\n    };\n    state.findings.obligations.forEach(v => rows.push([\"Obligation\", v, \"\"]));\n    state.findings.dates.forEach(v => rows.push([\"Date\", v, ctx(v)]));\n    state.findings.money.forEach(v => rows.push([\"Monetary\", v, ctx(v)]));\n\n    const csv = rows.map(r => r.map(x => `\"${String(x).replace(\/\"\/g,'\"\"')}\"`).join(\",\")).join(\"\\n\");\n    const baseName = (state.filename || 'contract').replace(\/\\.[^.]+$\/, '');\n    download(`${baseName}-findings.csv`, 'text\/csv;charset=utf-8', csv);\n  }\n\n  function copyFindingsSummary(){\n    const parts = [];\n    parts.push(`Obligations (${state.findings.obligations.length})`);\n    state.findings.obligations.forEach((s,i)=>parts.push(`  ${i+1}. ${s}`));\n    parts.push(\"\");\n    parts.push(`Dates (${state.findings.dates.length})`);\n    state.findings.dates.forEach((d,i)=>parts.push(`  ${i+1}. ${d}`));\n    parts.push(\"\");\n    parts.push(`Monetary (${state.findings.money.length})`);\n    state.findings.money.forEach((m,i)=>parts.push(`  ${i+1}. ${m}`));\n    const text = parts.join(\"\\n\");\n    navigator.clipboard.writeText(text).then(()=>{\n      setStatus(\"Summary copied to clipboard.\", \"success\");\n    }).catch(()=>{\n      setStatus(\"Could not copy to clipboard.\", \"error\");\n    });\n  }\n\n  function applyFilters(){\n    render();\n  }\n\n  function resetAll(){\n    state.rawText = \"\";\n    state.highlightedHTML = \"\";\n    state.findings = { obligations: [], dates: [], money: [] };\n    state.filename = null;\n    fileInput.value = \"\";\n    pasteInput.value = \"\";\n    viewer.classList.add('empty');\n    viewer.innerHTML = `<div>Upload a contract (PDF\/DOCX\/TXT) or paste text to begin.<\/div>`;\n    exportHtml.disabled = true;\n    exportCSV.disabled = true;\n    copySummary.disabled = true;\n    setStatus(\"Reset.\");\n  }\n\n  \/\/ Events\n  fileInput.addEventListener('change', (e) => {\n    const file = e.target.files?.[0];\n    if (file){ handleFile(file); }\n  });\n\n  filterObl.addEventListener('change', applyFilters);\n  filterDates.addEventListener('change', applyFilters);\n  filterMoney.addEventListener('change', applyFilters);\n\n  exportHtml.addEventListener('click', exportHighlightedHTML);\n  exportCSV.addEventListener('click', exportFindingsCSV);\n  copySummary.addEventListener('click', copyFindingsSummary);\n  resetBtn.addEventListener('click', resetAll);\n\n  pasteToggle.addEventListener('click', () => {\n    pasteArea.classList.toggle('hidden');\n  });\n  runOnPaste.addEventListener('click', () => {\n    const txt = pasteInput.value || \"\";\n    if (!txt.trim()){ setStatus(\"Paste some text first.\", \"error\"); return; }\n    state.filename = \"pasted-text.txt\";\n    analyzeAndHighlight(txt);\n    setStatus(\"Done. Highlighted pasted text.\", \"success\");\n  });\n\n  \/\/ Poll until external libs are ready (fast on CDN)\n  const tick = setInterval(()=>{\n    if (ready()){\n      clearInterval(tick);\n      ensurePDFWorker();\n      setStatus(\"Ready. Analysis runs in your browser\u2014files are not uploaded.\");\n    }\n  }, 100);\n})();\n<\/script>\nontract highlighter, contract obligations highlighter, highlight obligations in contracts, contract date highlighter, highlight dates in contracts, contract money highlighter, highlight monetary amounts in contracts, contract analyzer online, contract clause highlighter, contract terms highlighter, agreement highlighter, legal document highlighter, legal document analyzer, PDF contract highlighter, DOCX contract highlighter, TXT contract highlighter, client-side contract analyzer, offline contract analyzer, no upload contract tool, private contract analysis, contract keyword highlighter, highlight payment terms, payment terms finder, due date highlighter, contract deadline highlighter, milestone date highlighter, fees and charges highlighter, currency amount highlighter, dollar amount highlighter, AUD amount highlighter, USD amount highlighter, EUR amount highlighter, GBP amount highlighter, contract obligations extractor, obligations sentence finder, highlight \u201cshall\u201d and \u201cmust\u201d, obligations keyword detection, responsibilities in contracts, compliance clause highlighter, indemnity clause highlighter, warranty clause highlighter, representation clause highlighter, notification obligation highlighter, delivery obligation highlighter, service level obligation highlighter, performance obligation finder, termination clause dates, renewal date highlighter, notice period highlighter, \u201cno later than\u201d detector, \u201con or before\u201d date finder, net 30 payment finder, net 60 payment finder, contract timeline extractor, contract deadline detector, contract date parser, currency detector in contracts, money amounts extractor, numbers and figures highlighter, contract redlining helper, contract review tool, contract audit tool, legal review assistant, agreement review assistant, NDA highlighter, MSA highlighter, SaaS agreement highlighter, employment contract highlighter, consulting agreement highlighter, lease agreement highlighter, purchase agreement highlighter, vendor contract highlighter, supplier agreement highlighter, partnership agreement highlighter, service agreement highlighter, terms and conditions highlighter, statement of work highlighter, SOW date finder, invoice terms highlighter, penalty fee highlighter, late fee highlighter, liquidated damages finder, escrow amount finder, retainer amount finder, upfront fee highlighter, milestone payment finder, recurring payment terms, subscription fee highlighter, pricing clause highlighter, rate card highlighter, indexation clause finder, CPI adjustment finder, currency symbol detector, highlight $ amounts in PDF, highlight dates in PDF, highlight obligations in PDF, highlight $ amounts in DOCX, highlight dates in DOCX, highlight clauses in DOCX, paste contract text highlighter, browser-based contract tool, contract highlighting web app, free contract highlighter, simple contract highlighter, quick contract scan, automatic contract highlights, regex contract highlighter, rule-based contract analyzer, non-AI contract analyzer, lightweight contract review, legal ops tooling, contract management helper, contract review workflow, pre-signing contract checks, post-signing contract audit, obligations register starter, contract risk spotting, clause spotting tool, find critical contract terms, find key dates in agreements, find key amounts in agreements, highlight important contract terms, contract compliance tracking helper, contract summarization helper, generate highlighted HTML, export highlighted contracts, export contract findings CSV, copy contract summary, in-browser PDF text extraction, in-browser DOCX to text, Mammoth DOCX parser, PDF.js contract parser, local processing contract tool, privacy-first contract tool, contract data stays on device, no server upload contract tool, legal disclaimer included, contract review for small business, startup contract review tool, freelancer contract checker, procurement contract checker, vendor agreement checker, sales contract checker, real estate contract highlighter, construction contract highlighter, government contract helper, Australian contract highlighter, US contract highlighter, UK contract highlighter, EU contract highlighter, identify obligations dates money, contract highlight obligations dates money, contract date amount finder, contract clause detection, agreement clause detection, highlight shall must will, find payable amounts, detect currency figures, agreement deadline finder, contract milestone date finder, legal text highlighter, document highlighter online, text highlighter for contracts, contract analytics lite, contract parsing in browser, web contract review tool, highlight risks in contracts, quick contract insights, contract proofreading helper, contract QA tool, contract compliance precheck, agreement terms extractor, contract insights without AI, obligations dates monetary highlighter\n<\/body>\n<\/html>\n\n\n","protected":false},"excerpt":{"rendered":"<p>Contract Highlighter \u2013 Obligations, Dates &#038; Monetary Amounts Contract Highlighter Automatically highlight Obligations, Dates, and Monetary Amounts \ud83d\udcc4 Choose Contract [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_glsr_average":0,"_glsr_ranking":0,"_glsr_reviews":0,"footnotes":""},"class_list":["post-352","page","type-page","status-publish","hentry"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=\/wp\/v2\/pages\/352","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=352"}],"version-history":[{"count":2,"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=\/wp\/v2\/pages\/352\/revisions"}],"predecessor-version":[{"id":355,"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=\/wp\/v2\/pages\/352\/revisions\/355"}],"wp:attachment":[{"href":"https:\/\/bioskinetics.com.au\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=352"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}