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 & 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 & 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 & 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>
150183const 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
152185let 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// ───────────────────────────────────────────────
368423function 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 => / ^ a b s t r a c t $ / i. test ( h . trim ( ) ) ) ;
484+ const titleIdx = headers . findIndex ( h => / ^ t i t l e $ / 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
377581dropZone . addEventListener ( "click" , ( ) => fileInput . click ( ) ) ;
378582dropZone . addEventListener ( "dragover" , e => { e . preventDefault ( ) ; dropZone . classList . add ( "over" ) ; } ) ;
@@ -419,7 +623,11 @@ <h1>HighLit</h1>
419623}
420624
421625processBtn . 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
455665resetBtn . 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 ) ;
467681buildKwUI ( ) ;
468682</ script >
469683< div class ="container ">
0 commit comments