Skip to content

Commit 4531281

Browse files
committed
Added single abstract and excel input
1 parent 59b7918 commit 4531281

3 files changed

Lines changed: 246 additions & 28 deletions

File tree

HighLit.html

Lines changed: 233 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@
5555
.pp-li{ color:var(--muted); text-decoration:none; transition:color .15s; }
5656
.pp-li:hover{ color:var(--accent); }
5757
.disclaimer{ background:#fffbe6; border-bottom:1px solid #f0c040; padding:7px 20px; font-size:11px; color:#7a5c00; text-align:center; line-height:1.5; }
58+
.mode-tabs{ display:flex; margin-bottom:14px; border:1px solid var(--border); border-radius:6px; overflow:hidden; width:fit-content; }
59+
.mode-tab{ padding:6px 16px; font-size:12px; font-weight:500; cursor:pointer; background:#f1f5f9; color:var(--muted); border:none; transition:.15s; }
60+
.mode-tab.active{ background:var(--accent); color:#fff; }
61+
.mode-tab:not(:last-child){ border-right:1px solid var(--border); }
62+
.embase-col-info{ margin-top:10px; font-size:12px; color:var(--muted); }
63+
.embase-col-info select{ font-size:12px; padding:3px 8px; border:1px solid var(--border); border-radius:4px; background:var(--surface); color:var(--text); margin-left:6px; cursor:pointer; }
64+
#abstract-input{ width:100%; min-height:180px; border:1px solid var(--border); border-radius:var(--r); padding:12px; font-family:monospace; font-size:12px; line-height:1.6; resize:vertical; outline:none; color:var(--text); background:var(--surface); }
65+
#abstract-input:focus{ border-color:var(--accent); }
5866
</style>
5967
</head>
6068
<body>
@@ -69,14 +77,39 @@ <h1>HighLit</h1>
6977
<div class="container">
7078

7179
<div class="card">
72-
<div class="card-title"><span class="sn">1</span> Upload PubMed Literature File</div>
73-
<div class="drop" id="drop-zone">
74-
<div class="ic">📄</div>
75-
<p><strong>Click to browse</strong> or drag &amp; drop your PubMed <code>.txt</code> export</p>
76-
<p style="margin-top:5px;font-size:12px;">Any PubMed format — Abstract, MEDLINE/tagged, or plain text</p>
77-
<input type="file" id="file-input" accept=".txt">
80+
<div class="card-title"><span class="sn">1</span> <span id="card1-title">Enter Abstract</span></div>
81+
<div class="mode-tabs">
82+
<button class="mode-tab active" id="tab-single">Single Abstract</button>
83+
<button class="mode-tab" id="tab-batch">Batch (.txt)</button>
84+
<button class="mode-tab" id="tab-embase">Embase (.xlsx)</button>
85+
</div>
86+
<div id="panel-single">
87+
<textarea id="abstract-input" placeholder="Paste or type your abstract here…"></textarea>
88+
</div>
89+
<div id="panel-batch" style="display:none">
90+
<div class="drop" id="drop-zone">
91+
<div class="ic">📄</div>
92+
<p><strong>Click to browse</strong> or drag &amp; drop your PubMed <code>.txt</code> export</p>
93+
<p style="margin-top:5px;font-size:12px;">Any PubMed format — Abstract, MEDLINE/tagged, or plain text</p>
94+
<input type="file" id="file-input" accept=".txt">
95+
</div>
96+
<div id="file-info"></div>
97+
</div>
98+
<div id="panel-embase" style="display:none">
99+
<div class="drop" id="embase-drop-zone">
100+
<div class="ic">📊</div>
101+
<p><strong>Click to browse</strong> or drag &amp; drop your Embase <code>.xlsx</code> export</p>
102+
<p style="margin-top:5px;font-size:12px;">Use "MS Excel — Fields by Column" format from Embase</p>
103+
<input type="file" id="embase-file-input" accept=".xlsx">
104+
</div>
105+
<div class="embase-col-info" id="embase-col-info" style="display:none">
106+
<div id="embase-col-status"></div>
107+
<div id="embase-col-picker" style="display:none;margin-top:6px;">
108+
<label>Select Abstract column:</label>
109+
<select id="embase-abstract-col"><option value="">— choose —</option></select>
110+
</div>
111+
</div>
78112
</div>
79-
<div id="file-info"></div>
80113
</div>
81114

82115
<div class="card">
@@ -150,9 +183,10 @@ <h1>HighLit</h1>
150183
const STEMS = new Set(["injur","overdos","suicid","abus","misus","complicat","reproduct","intoxic","worsen","interact","carcinogen","mutagen","cancerogen","toxic","pharmacotoxic","cardiotoxic","neurotoxic","nephrotoxic","hepatotoxic","immunotoxic","cytotoxic","genotoxic","poison"]);
151184

152185
let cats = DEFAULTS.map(c => ({ ...c, keywords: [...c.keywords] }));
153-
let rawText = null;
154-
let lastBlob = null;
155-
let srcName = "output";
186+
let rawText = null;
187+
let lastBlob = null;
188+
let srcName = "output";
189+
let embaseData = null;
156190

157191
// ───────────────────────────────────────────────
158192
// KEYWORD TABLE UI
@@ -362,17 +396,187 @@ <h1>HighLit</h1>
362396
return zip.generateAsync({ type:"blob", mimeType:"application/vnd.openxmlformats-officedocument.wordprocessingml.document", compression:"DEFLATE" });
363397
}
364398

399+
async function makeEmbaseDocx(articles, patterns) {
400+
const NS = `xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"`;
401+
const body = [];
402+
for (const { title, abstract } of articles) {
403+
if (title) {
404+
const run = `<w:r><w:rPr><w:b/><w:bCs/><w:u w:val="single"/><w:sz w:val="24"/><w:szCs w:val="24"/></w:rPr><w:t xml:space="preserve">${xe(title)}</w:t></w:r>`;
405+
body.push(`<w:p><w:pPr><w:spacing w:before="200" w:after="60"/></w:pPr>${run}</w:p>`);
406+
}
407+
for (const line of abstract.split(/\r?\n/))
408+
body.push(oPara(line.trim() ? segRuns(segment(line, patterns)) : ""));
409+
body.push(`<w:p><w:pPr><w:spacing w:before="0" w:after="0"/></w:pPr></w:p>`);
410+
}
411+
const docXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document ${NS}><w:body>${body.join("")}<w:sectPr><w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/></w:sectPr></w:body></w:document>`;
412+
const zip = new JSZip();
413+
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`);
414+
zip.file("_rels/.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`);
415+
zip.file("word/document.xml", docXml);
416+
zip.file("word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`);
417+
return zip.generateAsync({ type:"blob", mimeType:"application/vnd.openxmlformats-officedocument.wordprocessingml.document", compression:"DEFLATE" });
418+
}
419+
365420
// ───────────────────────────────────────────────
366421
// UI
367422
// ───────────────────────────────────────────────
368423
function setStatus(msg, cls) { const el = document.getElementById("status"); el.textContent = msg; el.className = cls||""; }
369424

370-
const dropZone = document.getElementById("drop-zone");
371-
const fileInput = document.getElementById("file-input");
372-
const kwFileInput = document.getElementById("kw-file-input");
373-
const processBtn = document.getElementById("process-btn");
374-
const downloadBtn = document.getElementById("download-btn");
375-
const resetBtn = document.getElementById("reset-btn");
425+
const dropZone = document.getElementById("drop-zone");
426+
const fileInput = document.getElementById("file-input");
427+
const kwFileInput = document.getElementById("kw-file-input");
428+
const processBtn = document.getElementById("process-btn");
429+
const downloadBtn = document.getElementById("download-btn");
430+
const resetBtn = document.getElementById("reset-btn");
431+
const embaseDropZone = document.getElementById("embase-drop-zone");
432+
const embaseFileInput= document.getElementById("embase-file-input");
433+
434+
// ── MODE SWITCHING ──
435+
let inputMode = localStorage.getItem('highlit-mode') || 'single';
436+
437+
function applyMode(mode) {
438+
inputMode = mode;
439+
localStorage.setItem('highlit-mode', mode);
440+
document.getElementById('tab-single').classList.toggle('active', mode === 'single');
441+
document.getElementById('tab-batch').classList.toggle('active', mode === 'batch');
442+
document.getElementById('tab-embase').classList.toggle('active', mode === 'embase');
443+
document.getElementById('panel-single').style.display = mode === 'single' ? '' : 'none';
444+
document.getElementById('panel-batch').style.display = mode === 'batch' ? '' : 'none';
445+
document.getElementById('panel-embase').style.display = mode === 'embase' ? '' : 'none';
446+
const titles = { single: 'Enter Abstract', batch: 'Upload PubMed Literature File', embase: 'Upload Embase Excel Export' };
447+
document.getElementById('card1-title').textContent = titles[mode] || 'Input';
448+
rawText = null; lastBlob = null; srcName = 'output'; embaseData = null;
449+
fileInput.value = '';
450+
embaseFileInput.value = '';
451+
document.getElementById('abstract-input').value = '';
452+
document.getElementById('file-info').textContent = '';
453+
document.getElementById('embase-col-info').style.display = 'none';
454+
processBtn.disabled = true;
455+
downloadBtn.disabled = true;
456+
resetBtn.disabled = true;
457+
document.getElementById('preview-section').style.display = 'none';
458+
document.getElementById('preview-box').innerHTML = '';
459+
setStatus('', '');
460+
}
461+
462+
document.getElementById('tab-single').addEventListener('click', () => { if (inputMode !== 'single') applyMode('single'); });
463+
document.getElementById('tab-batch').addEventListener('click', () => { if (inputMode !== 'batch') applyMode('batch'); });
464+
document.getElementById('tab-embase').addEventListener('click', () => { if (inputMode !== 'embase') applyMode('embase'); });
465+
466+
// ── EMBASE MODE ──
467+
embaseDropZone.addEventListener('click', () => embaseFileInput.click());
468+
embaseDropZone.addEventListener('dragover', e => { e.preventDefault(); embaseDropZone.classList.add('over'); });
469+
embaseDropZone.addEventListener('dragleave', () => embaseDropZone.classList.remove('over'));
470+
embaseDropZone.addEventListener('drop', e => { e.preventDefault(); embaseDropZone.classList.remove('over'); const f = e.dataTransfer.files[0]; if (f) loadEmbaseFile(f); });
471+
embaseFileInput.addEventListener('change', e => { if (e.target.files[0]) loadEmbaseFile(e.target.files[0]); });
472+
document.getElementById('embase-abstract-col').addEventListener('change', finalizeEmbaseColumns);
473+
474+
async function loadEmbaseFile(file) {
475+
srcName = file.name.replace(/\.[^.]+$/, '');
476+
setStatus('Reading Embase file…', '');
477+
processBtn.disabled = true;
478+
try {
479+
const data = await parseEmbaseXlsx(file);
480+
if (!data || !data.rows.length) { setStatus('Could not read file — check it is an Embase "Fields by Column" .xlsx export', 'err'); return; }
481+
embaseData = data;
482+
const headers = data.headers;
483+
const abstractIdx = headers.findIndex(h => /^abstract$/i.test(h.trim()));
484+
const titleIdx = headers.findIndex(h => /^title$/i.test(h.trim()));
485+
embaseData.abstractColIdx = abstractIdx;
486+
embaseData.titleColIdx = titleIdx;
487+
const colInfo = document.getElementById('embase-col-info');
488+
const colStatus = document.getElementById('embase-col-status');
489+
const colPicker = document.getElementById('embase-col-picker');
490+
colInfo.style.display = '';
491+
if (abstractIdx >= 0) {
492+
colStatus.textContent = `✓ ${data.rows.length} article(s) — Abstract column detected` +
493+
(titleIdx >= 0 ? ` · Title column detected` : '');
494+
colPicker.style.display = 'none';
495+
finalizeEmbaseColumns();
496+
} else {
497+
colStatus.textContent = `${data.rows.length} article(s) loaded — Abstract column not found. Select it below:`;
498+
const select = document.getElementById('embase-abstract-col');
499+
select.innerHTML = '<option value="">— choose —</option>' +
500+
headers.map((h, i) => `<option value="${i}">${h || 'Column ' + (i + 1)}</option>`).join('');
501+
colPicker.style.display = '';
502+
setStatus('Select the Abstract column to continue.', '');
503+
}
504+
} catch(err) {
505+
setStatus('Error reading file: ' + err.message, 'err');
506+
}
507+
}
508+
509+
function finalizeEmbaseColumns() {
510+
if (!embaseData) return;
511+
if (embaseData.abstractColIdx < 0) {
512+
const val = parseInt(document.getElementById('embase-abstract-col').value);
513+
if (isNaN(val)) return;
514+
embaseData.abstractColIdx = val;
515+
}
516+
const articles = buildEmbaseArticles(embaseData);
517+
if (!articles.length) { setStatus('No abstract text found in selected column.', 'err'); return; }
518+
embaseData.articles = articles;
519+
rawText = articles.flatMap(a => [a.title, ...a.abstract.split(/\r?\n/), '']).join('\n');
520+
processBtn.disabled = false;
521+
resetBtn.disabled = false;
522+
downloadBtn.disabled = true;
523+
lastBlob = null;
524+
document.getElementById('preview-section').style.display = 'none';
525+
setStatus(`${articles.length} article(s) ready. Click 'Process File' to highlight.`, '');
526+
}
527+
528+
async function parseEmbaseXlsx(file) {
529+
const ab = await file.arrayBuffer();
530+
const zip = await JSZip.loadAsync(ab);
531+
const sharedStrings = [];
532+
const ssFile = zip.file("xl/sharedStrings.xml");
533+
if (ssFile) {
534+
const doc = new DOMParser().parseFromString(await ssFile.async("string"), "text/xml");
535+
doc.querySelectorAll("si").forEach(si =>
536+
sharedStrings.push([...si.querySelectorAll("t")].map(t => t.textContent).join("")));
537+
}
538+
const sheetFile = zip.file("xl/worksheets/sheet1.xml");
539+
if (!sheetFile) return null;
540+
const doc = new DOMParser().parseFromString(await sheetFile.async("string"), "text/xml");
541+
function colIdx(ref) {
542+
const m = ref && ref.match(/^([A-Z]+)/); if (!m) return -1;
543+
let ci = 0; for (let i = 0; i < m[1].length; i++) ci = ci * 26 + (m[1].charCodeAt(i) - 64); return ci - 1;
544+
}
545+
function cellVal(c) {
546+
const t = c.getAttribute("t");
547+
if (t === "inlineStr") return [...c.querySelectorAll("is t")].map(n => n.textContent).join("");
548+
if (t === "s") return sharedStrings[parseInt(c.querySelector("v")?.textContent)] || "";
549+
return c.querySelector("v")?.textContent || "";
550+
}
551+
const allRows = [...doc.querySelectorAll("row")];
552+
if (!allRows.length) return null;
553+
const headers = [];
554+
allRows[0].querySelectorAll("c").forEach(c => { const i = colIdx(c.getAttribute("r")); if (i >= 0) headers[i] = cellVal(c).trim(); });
555+
const maxCol = headers.length;
556+
for (let i = 0; i < maxCol; i++) if (!headers[i]) headers[i] = '';
557+
const rows = [];
558+
for (let ri = 1; ri < allRows.length; ri++) {
559+
const row = new Array(maxCol).fill('');
560+
allRows[ri].querySelectorAll("c").forEach(c => { const i = colIdx(c.getAttribute("r")); if (i >= 0 && i < maxCol) row[i] = cellVal(c).trim(); });
561+
if (row.some(v => v)) rows.push(row);
562+
}
563+
return { headers, rows };
564+
}
565+
566+
function buildEmbaseArticles({ rows, headers, abstractColIdx, titleColIdx }) {
567+
return rows.map(row => ({
568+
title: titleColIdx >= 0 ? (row[titleColIdx] || '').trim() : '',
569+
abstract: (row[abstractColIdx] || '').trim()
570+
})).filter(a => a.abstract);
571+
}
572+
573+
document.getElementById('abstract-input').addEventListener('input', () => {
574+
if (inputMode !== 'single') return;
575+
const hasText = document.getElementById('abstract-input').value.trim().length > 0;
576+
processBtn.disabled = !hasText;
577+
resetBtn.disabled = !hasText;
578+
if (!hasText) downloadBtn.disabled = true;
579+
});
376580

377581
dropZone.addEventListener("click", () => fileInput.click());
378582
dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("over"); });
@@ -419,7 +623,11 @@ <h1>HighLit</h1>
419623
}
420624

421625
processBtn.addEventListener("click", async () => {
422-
if (!rawText) return;
626+
if (inputMode === 'single') {
627+
rawText = document.getElementById('abstract-input').value;
628+
srcName = 'Abstract';
629+
}
630+
if (!rawText || !rawText.trim()) return;
423631
processBtn.disabled = true;
424632
setStatus("Highlighting…", "");
425633
try {
@@ -435,7 +643,9 @@ <h1>HighLit</h1>
435643
: `— ${lines.length.toLocaleString()} lines · ${hlCount.toLocaleString()} highlight(s)`;
436644
sec.scrollIntoView({ behavior:"smooth", block:"start" });
437645
setStatus("Generating Word document…", "");
438-
lastBlob = await makeDocx(lines, patterns);
646+
lastBlob = (inputMode === 'embase' && embaseData?.articles)
647+
? await makeEmbaseDocx(embaseData.articles, patterns)
648+
: await makeDocx(lines, patterns);
439649
downloadBtn.disabled = false;
440650
setStatus(`✓ Done — ${hlCount.toLocaleString()} highlight(s) applied. Click Download to save.`, "ok");
441651
} catch(err) {
@@ -453,9 +663,12 @@ <h1>HighLit</h1>
453663
});
454664

455665
resetBtn.addEventListener("click", () => {
456-
rawText = null; lastBlob = null; srcName = "output";
666+
rawText = null; lastBlob = null; srcName = "output"; embaseData = null;
457667
fileInput.value = "";
668+
embaseFileInput.value = "";
669+
document.getElementById('abstract-input').value = '';
458670
document.getElementById("file-info").textContent = "";
671+
document.getElementById('embase-col-info').style.display = 'none';
459672
processBtn.disabled = true;
460673
downloadBtn.disabled = true;
461674
resetBtn.disabled = true;
@@ -464,6 +677,7 @@ <h1>HighLit</h1>
464677
setStatus("", "");
465678
});
466679

680+
applyMode(inputMode);
467681
buildKwUI();
468682
</script>
469683
<div class="container">

0 commit comments

Comments
 (0)