<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Darat - Universal Citation‑Miner</title>
<style>
:root {
--bg-sage: #E8EDD9;
--panel-bg: #FAF7F2;
--text-main: #2F2F2F;
--olive: #6B7A3A;
--terracotta: #C46A4A;
--line-soft: #D9DCC8;
--badge-bg: #333333;
--badge-text: #FFFFFF;
}
body {
margin: 0;
padding: 24px;
background: var(--bg-sage);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text-main);
}
h1, h2 {
font-family: Georgia, "Times New Roman", serif;
margin: 0 0 12px 0;
color: var(--text-main);
}
h1 { font-size: 26px; letter-spacing: 0.03em; }
h2 { font-size: 18px; letter-spacing: 0.02em; }
.panel {
background: var(--panel-bg);
border-radius: 14px;
padding: 18px 20px;
margin-bottom: 20px;
box-shadow: 0 10px 24px rgba(0,0,0,0.06);
border: 1px solid rgba(0,0,0,0.03);
}
.pill {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
background: #E1E6CF;
color: var(--text-main);
}
button {
border-radius: 999px;
border: none;
padding: 8px 18px;
margin-left: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
background: var(--olive);
color: #fff;
transition: background 0.15s ease, transform 0.1s ease;
}
button:hover:not(:disabled) {
background: #7C8B45;
transform: translateY(-1px);
}
.accordion {
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
border: 1px solid rgba(0,0,0,0.05);
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: #F2F4E4;
cursor: pointer;
}
.accordion-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: var(--badge-bg);
color: var(--badge-text);
}
.accordion-body {
display: none;
padding: 10px 14px 14px 14px;
background: #FBFAF6;
}
.accordion-body.open { display: block; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 6px;
font-size: 13px;
}
th, td {
padding: 10px 10px;
border-bottom: 1px solid var(--line-soft);
vertical-align: top;
}
th {
text-align: left;
font-size: 12px;
text-transform: uppercase;
color: #555;
}
tbody tr:nth-child(odd) { background: #FBFAF6; }
tbody tr:nth-child(even) { background: #F5F6EB; }
.status-ok { color: var(--olive); font-weight: 600; }
.status-fail { color: var(--terracotta); font-weight: 600; }
.confidence-high { color: var(--olive); font-weight: 600; }
.confidence-medium { color: var(--terracotta); font-weight: 600; }
a.doi-link {
color: var(--olive);
text-decoration: none;
font-size: 12px;
}
</style>
</head>
<body>
<div class="panel">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
<div>
<h1>Darat's Universal Citation‑Miner</h1>
<div style="font-size:13px; max-width:520px;">
Extract full references, inline citations, numeric citations, DOIs, footnotes, and more from any PDF.
</div>
</div>
<div style="display:flex; flex-direction:column; align-items:flex-end; gap:6px;">
<span class="pill">Worker‑free · Local‑friendly</span>
<span class="pill">CrossRef + OpenAlex</span>
</div>
</div>
<div style="margin-top:14px;">
<label style="font-size:14px; font-weight:500;">
Load PDF:
<input type="file" id="pdfFile" accept="application/pdf">
</label>
<button id="extractBtn" disabled>Mine Citations</button>
</div>
<div id="status" style="margin-top:10px; font-size:13px;"></div>
<pre id="error-log" style="margin-top:8px; font-size:12px; color:#C46A4A; white-space:pre-wrap; max-height:140px; overflow-y:auto;"></pre>
</div>
<div class="panel" id="results-panel" style="display:none;">
<h2>Citations</h2>
<div id="accordion-full" class="accordion">
<div class="accordion-header">
<div class="accordion-title">
<span>Full References</span>
<span class="badge" id="badge-full">0</span>
</div>
<div>▼</div>
</div>
<div class="accordion-body" id="body-full"></div>
</div>
<div id="accordion-inline" class="accordion">
<div class="accordion-header">
<div class="accordion-title">
<span>Inline Citations</span>
<span class="badge" id="badge-inline">0</span>
</div>
<div>▼</div>
</div>
<div class="accordion-body" id="body-inline"></div>
</div>
<div id="accordion-numeric" class="accordion">
<div class="accordion-header">
<div class="accordion-title">
<span>Numeric Citations</span>
<span class="badge" id="badge-numeric">0</span>
</div>
<div>▼</div>
</div>
<div class="accordion-body" id="body-numeric"></div>
</div>
<div id="accordion-doi" class="accordion">
<div class="accordion-header">
<div class="accordion-title">
<span>DOIs</span>
<span class="badge" id="badge-doi">0</span>
</div>
<div>▼</div>
</div>
<div class="accordion-body" id="body-doi"></div>
</div>
<div id="accordion-footnote" class="accordion">
<div class="accordion-header">
<div class="accordion-title">
<span>Footnotes</span>
<span class="badge" id="badge-footnote">0</span>
</div>
<div>▼</div>
</div>
<div class="accordion-body" id="body-footnote"></div>
</div>
<div id="accordion-other" class="accordion">
<div class="accordion-header">
<div class="accordion-title">
<span>Other / Unclassified</span>
<span class="badge" id="badge-other">0</span>
</div>
<div>▼</div>
</div>
<div class="accordion-body" id="body-other"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
<script>
let pdfArrayBuffer = null;
const fileInput = document.getElementById("pdfFile");
const extractBtn = document.getElementById("extractBtn");
const statusEl = document.getElementById("status");
const errorLogEl = document.getElementById("error-log");
const resultsPanel = document.getElementById("results-panel");
const badgeFull = document.getElementById("badge-full");
const badgeInline = document.getElementById("badge-inline");
const badgeNumeric = document.getElementById("badge-numeric");
const badgeDoi = document.getElementById("badge-doi");
const badgeFootnote = document.getElementById("badge-footnote");
const badgeOther = document.getElementById("badge-other");
const bodyFull = document.getElementById("body-full");
const bodyInline = document.getElementById("body-inline");
const bodyNumeric = document.getElementById("body-numeric");
const bodyDoi = document.getElementById("body-doi");
const bodyFootnote = document.getElementById("body-footnote");
const bodyOther = document.getElementById("body-other");
function logError(msg, err) {
errorLogEl.textContent += msg + (err ? " :: " + err : "") + "\n";
console.error(msg, err);
}
/* -----------------------------------------
ACCORDION BEHAVIOUR
------------------------------------------*/
document.querySelectorAll(".accordion-header").forEach(header => {
header.addEventListener("click", () => {
const body = header.nextElementSibling;
body.classList.toggle("open");
});
});
/* -----------------------------------------
FILE LOADING
------------------------------------------*/
fileInput.addEventListener("change", async e => {
const file = e.target.files[0];
if (!file) return;
try {
pdfArrayBuffer = await file.arrayBuffer();
extractBtn.disabled = false;
statusEl.textContent = "PDF loaded.";
errorLogEl.textContent = "";
resultsPanel.style.display = "none";
} catch (err) {
logError("Error reading file", err);
}
});
/* -----------------------------------------
EXTRACTION WORKFLOW
------------------------------------------*/
extractBtn.addEventListener("click", async () => {
if (!pdfArrayBuffer) return;
extractBtn.disabled = true;
statusEl.textContent = "Extracting text…";
errorLogEl.textContent = "";
resultsPanel.style.display = "none";
try {
const pdf = await pdfjsLib.getDocument({ data: pdfArrayBuffer }).promise;
let pages = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const text = content.items.map(x => x.str).join(" ");
pages.push(text);
}
const cleaned = cleanHeaders(pages);
statusEl.textContent = "Mining citations…";
const mined = mineCitations(cleaned);
await verifyCitations(mined);
renderGroups(mined);
resultsPanel.style.display = "block";
statusEl.textContent = "Done.";
} catch (err) {
logError("Extraction error", err);
statusEl.textContent = "Error during extraction.";
}
extractBtn.disabled = false;
});
/* -----------------------------------------
HEADER CLEANER
------------------------------------------*/
function cleanHeaders(pages) {
const heads = pages.map(p => p.slice(0, 120).trim());
const freq = {};
heads.forEach(h => {
if (h.length < 20) return;
const key = h.slice(0, 60);
freq[key] = (freq[key] || 0) + 1;
});
let header = null;
let max = 0;
for (const k in freq) {
if (freq[k] > max && freq[k] >= 3) {
max = freq[k];
header = k;
}
}
let text = pages.join("\n\n");
if (header) {
const safe = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(safe + ".*?(?=\\n|$)", "g");
text = text.replace(re, "");
}
text = text.replace(/Page Proof.*?(?=\n|$)/gi, "");
text = text.replace(/Compositor Name:.*?(?=\n|$)/gi, "");
text = text.replace(/\bpage\s+\d+\b/gi, "");
text = text.replace(/\b\d{1,2}\.\d{1,2}\.\d{2,4}\b/g, "");
text = text.replace(/\b\d{1,2}:\d{2}(am|pm)\b/gi, "");
return text.replace(/\s+/g, " ").trim();
}
/* -----------------------------------------
CITATION MINER
------------------------------------------*/
function mineCitations(text) {
const fullRefs = new Set();
const inlineCits = new Set();
const numericCits = new Set();
const doiCits = new Set();
const footnoteCits = new Set();
const otherCits = new Set();
function accept(str) {
const s = str.trim();
if (s.length < 25 || s.length > 350) return false;
const bad = ["page proof", "compositor", "copyright", "printed in"];
return !bad.some(w => s.toLowerCase().includes(w));
}
const doiRegex = /10\.\d{4,9}\/[-._;()/:A-Za-z0-9]+/g;
let m;
while ((m = doiRegex.exec(text)) !== null) doiCits.add(m[0]);
const sentences = text.split(/(?<=[.!?])\s+(?=[A-Z])/g);
const fullRefRegex = /[A-Z][a-z]+,\s+[A-Z](?:\.[A-Z]\.)?.*?\b(19|20)\d{2}.*?(?=[.!?])/g;
sentences.forEach(s => {
let mm;
while ((mm = fullRefRegex.exec(s)) !== null) {
if (accept(mm[0])) fullRefs.add(mm[0]);
}
});
const inline1 = /\([A-Z][A-Za-z]+(?:\s+et al\.)?,\s*(19|20)\d{2}\)/g;
const inline2 = /[A-Z][A-Za-z]+(?:\s+et al\.)?\s*\((19|20)\d{2}\)/g;
sentences.forEach(s => {
let mm;
while ((mm = inline1.exec(s)) !== null) if (accept(mm[0])) inlineCits.add(mm[0]);
while ((mm = inline2.exec(s)) !== null) if (accept(mm[0])) inlineCits.add(mm[0]);
});
const numRegex = /\[\d+(?:\s*,\s*\d+)*\]/g;
sentences.forEach(s => {
let mm;
while ((mm = numRegex.exec(s)) !== null) if (accept(mm[0])) numericCits.add(mm[0]);
});
const footRegex = /\b\d{1,3}\s+[A-Z][a-z]+.*?\b(19|20)\d{2}.*?(?=[.!?])/g;
sentences.forEach(s => {
let mm;
while ((mm = footRegex.exec(s)) !== null) if (accept(mm[0])) footnoteCits.add(mm[0]);
});
sentences.forEach(s => {
const hasYear = /(19|20)\d{2}/.test(s);
const hasName = /[A-Z][a-z]+,\s+[A-Z]\./.test(s);
if (hasYear && hasName && accept(s)) {
if (![...fullRefs].includes(s) && ![...footnoteCits].includes(s)) {
otherCits.add(s);
}
}
});
return {
full: [...fullRefs],
inline: [...inlineCits],
numeric: [...numericCits],
doi: [...doiCits],
footnote: [...footnoteCits],
other: [...otherCits]
};
}
/* -----------------------------------------
VERIFICATION (CrossRef + OpenAlex)
------------------------------------------*/
async function verifyCitations(mined) {
mined._verifiedFull = [];
mined._verifiedDoi = [];
for (const ref of mined.full) {
const res = await checkReference(ref);
mined._verifiedFull.push({ reference: ref, ...res });
}
for (const doi of mined.doi) {
const res = await checkReference(doi);
mined._verifiedDoi.push({ reference: doi, ...res });
}
}
function extractPossibleDOI(ref) {
const m = ref.match(/10\.\d{4,9}\/[-._;()/:A-Za-z0-9]+/);
return m ? m[0] : null;
}
async function checkReference(ref) {
let doi = extractPossibleDOI(ref);
let sources = [];
let score = 0;
let cross = null;
let alex = null;
try {
if (doi) {
const r = await fetch("https://api.crossref.org/works/" + encodeURIComponent(doi));
if (r.ok) cross = (await r.json()).message;
}
if (!cross) {
const q = encodeURIComponent(ref.slice(0, 200));
const r = await fetch("https://api.crossref.org/works?query.bibliographic=" + q + "&rows=1");
if (r.ok) {
const d = await r.json();
if (d.message.items.length) cross = d.message.items[0];
}
}
} catch {}
if (cross) {
sources.push("CrossRef");
if (!doi && cross.DOI) doi = cross.DOI;
score += 0.4;
}
try {
const q = encodeURIComponent(ref.slice(0, 200));
const r = await fetch("https://api.openalex.org/works?search=" + q + "&per-page=1");
if (r.ok) {
const d = await r.json();
if (d.results.length) alex = d.results[0];
}
} catch {}
if (alex) {
sources.push("OpenAlex");
if (!doi && alex.doi) doi = alex.doi.replace(/^https?:\/\/doi.org\//, "");
score += 0.3;
}
const status = score >= 0.4 ? "ok" : "fail";
const confidence = score >= 0.75 ? "High" : score >= 0.4 ? "Medium" : "Low";
return { status, confidence, doi, sources, score };
}
/* -----------------------------------------
RENDERING
------------------------------------------*/
function escapeHtml(str) {
return str
.replace(/&/g,"&")
.replace(/</g,"<")
.replace(/>/g,">");
}
function renderGroups(mined) {
badgeFull.textContent = mined._verifiedFull.length;
bodyFull.innerHTML = renderVerifiedTable(mined._verifiedFull);
badgeInline.textContent = mined.inline.length;
bodyInline.innerHTML = renderSimpleTable(mined.inline);
badgeNumeric.textContent = mined.numeric.length;
bodyNumeric.innerHTML = renderSimpleTable(mined.numeric);
badgeDoi.textContent = mined._verifiedDoi.length;
bodyDoi.innerHTML = renderVerifiedTable(mined._verifiedDoi);
badgeFootnote.textContent = mined.footnote.length;
bodyFootnote.innerHTML = renderSimpleTable(mined.footnote);
badgeOther.textContent = mined.other.length;
bodyOther.innerHTML = renderSimpleTable(mined.other);
}
function renderVerifiedTable(items) {
if (!items.length) {
return '<div style="font-size:12px; color:#666;">No items detected.</div>';
}
let html = `
<table>
<thead>
<tr>
<th>Status</th>
<th>Confidence</th>
<th>Reference</th>
<th>DOI</th>
<th>Sources</th>
</tr>
</thead>
<tbody>
`;
items.forEach(r => {
const statusClass =
r.status === "ok" ? "status-ok" :
r.status === "fail" ? "status-fail" : "";
const confClass =
r.confidence === "High" ? "confidence-high" :
r.confidence === "Medium" ? "confidence-medium" : "";
let doiHtml = "—";
if (r.doi) {
const url = "https://doi.org/" + r.doi;
doiHtml = `<a class="doi-link" href="${url}" target="_blank">${escapeHtml(r.doi)}</a>`;
}
const src = r.sources && r.sources.length ? r.sources.join(" + ") : "—";
html += `
<tr>
<td class="${statusClass}">${escapeHtml(r.status)}</td>
<td class="${confClass}">${escapeHtml(r.confidence)}</td>
<td>${escapeHtml(r.reference)}</td>
<td>${doiHtml}</td>
<td>${escapeHtml(src)}</td>
</tr>
`;
});
html += "</tbody></table>";
return html;
}
function renderSimpleTable(list) {
if (!list.length) {
return '<div style="font-size:12px; color:#666;">No items detected.</div>';
}
let html = `
<table>
<thead><tr><th>Citation</th></tr></thead>
<tbody>
`;
list.forEach(item => {
html += `<tr><td>${escapeHtml(item)}</td></tr>`;
});
html += "</tbody></table>";
return html;
}
</script>
</body>
</html>