|
<!DOCTYPE html> |
|
<html lang="en"> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Latin Vulgate Text Editor with Marginalia</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: 'Georgia', serif; |
|
line-height: 1.6; |
|
background-color: #f8f7f5; |
|
color: #333; |
|
} |
|
|
|
.container { |
|
display: grid; |
|
grid-template-columns: 1fr 250px; |
|
min-height: 100vh; |
|
padding: 20px; |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
gap: 20px; |
|
} |
|
|
|
.main-content { |
|
background: white; |
|
padding: 40px; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
|
overflow-y: auto; |
|
position: relative; |
|
} |
|
|
|
.margin-area { |
|
background: #f8f7f5; |
|
border: 1px solid #e5e5e5; |
|
border-radius: 8px; |
|
padding: 20px; |
|
position: sticky; |
|
top: 20px; |
|
height: fit-content; |
|
max-height: calc(100vh - 40px); |
|
overflow-y: auto; |
|
} |
|
|
|
.margin-area h3 { |
|
margin: 0 0 15px 0; |
|
color: #2c3e50; |
|
font-size: 1.1em; |
|
border-bottom: 1px solid #ddd; |
|
padding-bottom: 10px; |
|
} |
|
|
|
.margin-note { |
|
background: white; |
|
border: 1px solid #ddd; |
|
border-left: 3px solid #3498db; |
|
border-radius: 4px; |
|
padding: 10px; |
|
margin-bottom: 10px; |
|
font-size: 0.85em; |
|
position: relative; |
|
cursor: pointer; |
|
} |
|
|
|
.margin-note:hover { |
|
background: #f8f9fa; |
|
border-left-color: #2980b9; |
|
} |
|
|
|
.margin-note-reference { |
|
font-weight: bold; |
|
color: #2c3e50; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.margin-note-text { |
|
color: #555; |
|
line-height: 1.3; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.margin-note-similarity { |
|
font-size: 0.75em; |
|
color: #777; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.margin-note-remove { |
|
position: absolute; |
|
top: 5px; |
|
right: 5px; |
|
background: #e74c3c; |
|
color: white; |
|
border: none; |
|
border-radius: 50%; |
|
width: 18px; |
|
height: 18px; |
|
font-size: 10px; |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.margin-note-remove:hover { |
|
background: #c0392b; |
|
} |
|
|
|
.margin-note.highlighted { |
|
background: #e8f4fd; |
|
border-left-color: #3498db; |
|
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2); |
|
} |
|
|
|
.sidebar { |
|
display: none; |
|
} |
|
|
|
.header { |
|
text-align: center; |
|
margin-bottom: 30px; |
|
padding-bottom: 20px; |
|
border-bottom: 2px solid #eee; |
|
} |
|
|
|
.header h1 { |
|
color: #2c3e50; |
|
font-size: 2.5em; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.controls { |
|
display: flex; |
|
gap: 15px; |
|
margin-bottom: 20px; |
|
align-items: center; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.control-group { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 5px; |
|
} |
|
|
|
label { |
|
font-weight: bold; |
|
font-size: 0.9em; |
|
color: #555; |
|
} |
|
|
|
select, input, button { |
|
padding: 8px 12px; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
font-size: 14px; |
|
} |
|
|
|
textarea#custom-text { |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
padding: 10px; |
|
font-family: 'Georgia', serif; |
|
font-size: 14px; |
|
line-height: 1.4; |
|
outline: none; |
|
transition: border-color 0.3s; |
|
resize: vertical; |
|
} |
|
|
|
textarea#custom-text:focus { |
|
border-color: #3498db; |
|
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3); |
|
} |
|
|
|
button { |
|
background: #3498db; |
|
color: white; |
|
cursor: pointer; |
|
border: none; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
button:hover { |
|
background: #2980b9; |
|
} |
|
|
|
.export-btn { |
|
background: #27ae60; |
|
margin-left: auto; |
|
} |
|
|
|
.export-btn:hover { |
|
background: #219a52; |
|
} |
|
|
|
.text-content { |
|
font-size: 1.1em; |
|
line-height: 1.8; |
|
text-align: justify; |
|
user-select: text; |
|
cursor: text; |
|
} |
|
|
|
.text-content p { |
|
margin-bottom: 20px; |
|
text-indent: 2em; |
|
} |
|
|
|
.selected-text { |
|
background-color: #ffffcc; |
|
padding: 2px 4px; |
|
border-radius: 3px; |
|
position: relative; |
|
} |
|
|
|
.marginalia-marker { |
|
background-color: #e8f4fd; |
|
border-left: 3px solid #3498db; |
|
padding: 2px 4px; |
|
border-radius: 3px; |
|
position: relative; |
|
cursor: pointer; |
|
} |
|
|
|
.marginalia-indicator { |
|
position: absolute; |
|
top: -8px; |
|
right: -8px; |
|
background: #3498db; |
|
color: white; |
|
border-radius: 50%; |
|
width: 18px; |
|
height: 18px; |
|
font-size: 10px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-weight: bold; |
|
z-index: 10; |
|
} |
|
|
|
.marginalia-indicator.multiple { |
|
background: #e74c3c; |
|
} |
|
|
|
.marginalia-tooltip { |
|
position: absolute; |
|
background: white; |
|
border: 1px solid #ddd; |
|
border-radius: 6px; |
|
padding: 12px; |
|
box-shadow: 0 4px 15px rgba(0,0,0,0.15); |
|
z-index: 1000; |
|
max-width: 400px; |
|
min-width: 300px; |
|
display: none; |
|
top: 100%; |
|
left: 0; |
|
margin-top: 5px; |
|
font-size: 0.9em; |
|
} |
|
|
|
.marginalia-tooltip.show { |
|
display: block; |
|
} |
|
|
|
.marginalia-tooltip-item { |
|
margin-bottom: 12px; |
|
padding-bottom: 12px; |
|
border-bottom: 1px solid #eee; |
|
} |
|
|
|
.marginalia-tooltip-item:last-child { |
|
margin-bottom: 0; |
|
padding-bottom: 0; |
|
border-bottom: none; |
|
} |
|
|
|
.marginalia-tooltip-reference { |
|
font-weight: bold; |
|
color: #2c3e50; |
|
margin-bottom: 6px; |
|
font-size: 0.95em; |
|
} |
|
|
|
.marginalia-tooltip-text { |
|
font-size: 0.9em; |
|
line-height: 1.4; |
|
margin-bottom: 6px; |
|
color: #444; |
|
} |
|
|
|
.marginalia-tooltip-similarity { |
|
font-size: 0.8em; |
|
color: #666; |
|
} |
|
|
|
|
|
|
|
.popup { |
|
position: fixed; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
background: white; |
|
padding: 30px; |
|
border-radius: 12px; |
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
z-index: 1000; |
|
max-width: 90%; |
|
max-height: 80%; |
|
overflow-y: auto; |
|
display: none; |
|
} |
|
|
|
.popup-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background: rgba(0,0,0,0.5); |
|
z-index: 999; |
|
display: none; |
|
} |
|
|
|
.popup-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 20px; |
|
padding-bottom: 10px; |
|
border-bottom: 1px solid #eee; |
|
} |
|
|
|
.popup-header h3 { |
|
margin: 0; |
|
flex-shrink: 0; |
|
} |
|
|
|
.popup-header > div { |
|
display: flex; |
|
align-items: center; |
|
flex-wrap: wrap; |
|
gap: 10px; |
|
} |
|
|
|
.close-btn { |
|
background: #e74c3c; |
|
color: white; |
|
border: none; |
|
padding: 8px 12px; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
} |
|
|
|
.search-results { |
|
max-height: 400px; |
|
overflow-y: auto; |
|
} |
|
|
|
.result-item { |
|
padding: 15px; |
|
border: 1px solid #eee; |
|
margin-bottom: 10px; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
transition: background-color 0.2s; |
|
} |
|
|
|
.result-item:hover { |
|
background-color: #f8f9fa; |
|
} |
|
|
|
.result-reference { |
|
font-weight: bold; |
|
color: #2c3e50; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.result-text { |
|
font-style: italic; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.result-similarity { |
|
font-size: 0.9em; |
|
color: #666; |
|
} |
|
|
|
|
|
|
|
.loading { |
|
text-align: center; |
|
padding: 20px; |
|
color: #666; |
|
} |
|
|
|
.error { |
|
color: #e74c3c; |
|
background: #fdf2f2; |
|
padding: 10px; |
|
border-radius: 4px; |
|
margin: 10px 0; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.container { |
|
padding: 10px; |
|
grid-template-columns: 1fr; |
|
gap: 10px; |
|
} |
|
|
|
.main-content { |
|
padding: 20px; |
|
} |
|
|
|
.margin-area { |
|
order: -1; |
|
position: static; |
|
max-height: 300px; |
|
} |
|
|
|
.controls { |
|
flex-direction: column; |
|
align-items: stretch; |
|
} |
|
|
|
.control-group { |
|
width: 100%; |
|
} |
|
|
|
#custom-text { |
|
min-width: auto !important; |
|
width: 100% !important; |
|
font-size: 16px; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
outline: none; |
|
transition: border-color 0.3s; |
|
} |
|
|
|
#custom-text:focus { |
|
border-color: #3498db; |
|
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3); |
|
} |
|
|
|
.popup { |
|
padding: 20px; |
|
max-width: 95%; |
|
} |
|
|
|
.marginalia-tooltip { |
|
max-width: 320px; |
|
min-width: 280px; |
|
font-size: 0.85em; |
|
} |
|
|
|
.margin-note { |
|
font-size: 0.8em; |
|
padding: 8px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="main-content"> |
|
<div class="header"> |
|
<h1>Latin Vulgate Quote Finder</h1> |
|
<p>Paste your own text or use the sample below. Select text to search for similar passages and add marginalia</p> |
|
|
|
<div id="api-notice" style="background: #d4edda; color: #155724; padding: 15px; border-radius: 4px; margin: 10px 0; font-size: 0.9em; border-left: 4px solid #28a745;"> |
|
<strong>✅ API Configuration Fixed</strong><br> |
|
The correct API endpoints have been identified. Your Gradio Space should now work properly with this interface. |
|
|
|
<div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.5); border-radius: 4px;"> |
|
<strong>Technical Details:</strong><br> |
|
• Using proper Gradio API pattern: POST → GET with event streaming<br> |
|
• Endpoint: <code>/gradio_api/call/predict</code><br> |
|
• Your app.py has the correct <code>api_name="predict"</code> configuration |
|
</div> |
|
|
|
<div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.5); border-radius: 4px;"> |
|
<strong>If you still see connection issues:</strong><br> |
|
Try refreshing this page or restarting your <a href="https://huggingface.co/spaces/medieval-data/latin-vulgate" target="_blank" style="color: #1d4ed8;">HuggingFace Space</a> |
|
</div> |
|
</div> |
|
|
|
<div id="connection-status" style="margin-top: 10px; padding: 8px; border-radius: 4px; font-size: 0.9em;"> |
|
<span id="status-indicator">🔄</span> <span id="status-text">Connecting to search backend...</span> |
|
</div> |
|
</div> |
|
|
|
<div class="controls"> |
|
<div class="control-group"> |
|
<label for="custom-text">Paste Your Text:</label> |
|
<textarea id="custom-text" placeholder="Paste your Latin text here to annotate it..." rows="4" style="width: 100%; min-width: 300px; resize: vertical; font-family: 'Georgia', serif; line-height: 1.4; padding: 10px;"></textarea> |
|
<div style="display: flex; gap: 10px; margin-top: 5px;"> |
|
<button onclick="loadCustomText()" style="background: #27ae60;">Load Text</button> |
|
<button onclick="resetToSample()" style="background: #95a5a6;">Reset to Sample</button> |
|
<button onclick="clearTextArea()" style="background: #e74c3c;">Clear</button> |
|
</div> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label for="search-method">Search Method:</label> |
|
<select id="search-method"> |
|
<option value="vector">Vector Search</option> |
|
<option value="bm25">BM25 Search</option> |
|
<option value="hybrid">Hybrid Search</option> |
|
</select> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label for="result-limit">Results Limit:</label> |
|
<input type="number" id="result-limit" min="1" max="50" value="10"> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label for="book-filter">Filter by Books:</label> |
|
<select id="book-filter" multiple style="height: 100px;"> |
|
|
|
</select> |
|
</div> |
|
|
|
<button class="export-btn" onclick="exportToTEI()">Export as XML</button> |
|
</div> |
|
|
|
<div class="text-content" id="text-content"> |
|
<h2>De Imitatione Christi - Sample Text</h2> |
|
<p>Qui sequitur me, non ambulat in tenebris, dicit Dominus. Haec sunt verba Christi, quibus admonemur, quatenus vitam ejus et mores imitemur, si volumus veraciter illuminari, et ab omni caecitate cordis liberari. Summum igitur studium nostrum sit, in vita Jesu Christi meditari.</p> |
|
|
|
<p>Doctrina Christi omnes doctrinas sanctorum praecellit, et qui spiritum habet, inveniet ibi manna absconditum. Sed contingit, quod multi ex frequenti auditione Evangelii, parum desiderium sentiunt: quia spiritum Christi non habent. Qui autem vult plene et sapide verba Christi intelligere, oportet ut totam vitam suam illi studeat conformare.</p> |
|
|
|
<p>Quid tibi prodest alta de Trinitate disputare, si cares humilitate, unde displiceas Trinitati? Vere alta verba non faciunt sanctum et justum; sed virtuosa vita efficit Deo carum. Opto magis sentire compunctionem, quam scire ejus definitionem.</p> |
|
|
|
<p>Si scires totam Bibliam exterius, et omnium philosophorum dicta, quid totum prodesset sine caritate et gratia Dei? Vanitas vanitatum, et omnia vanitas, praeter amare Deum, et illi soli servire. Haec est summa sapientia: per contemptum mundi tendere ad regna caelestia.</p> |
|
|
|
<p>Vanitas igitur est, honores perishables sectari, et ad alta loca ascendere. Vanitas est, carnis desideria sequi, et illud desiderare unde oporteat postea gravius puniri. Vanitas est, longam vitam optare, et de bona vita parum curare. Vanitas est, praesentem vitam tantum attendere, et quae futura sunt non prospicere.</p> |
|
</div> |
|
</div> |
|
<div class="margin-area"> |
|
<h3>Marginalia</h3> |
|
<div id="marginalia-list"> |
|
<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="popup-overlay" id="popup-overlay" onclick="closePopup()"></div> |
|
<div class="popup" id="search-popup"> |
|
<div class="popup-header"> |
|
<div> |
|
<h3>Similar Passages</h3> |
|
<div id="query-text" style="margin-top: 5px; font-size: 0.9em; color: #666; font-style: italic; max-width: 400px; line-height: 1.3;"></div> |
|
</div> |
|
<div> |
|
<small style="color: #666; margin-right: 15px;">Click results to add marginalia</small> |
|
<button class="close-btn" onclick="closePopup()">Close</button> |
|
</div> |
|
</div> |
|
<div id="search-results" class="search-results"> |
|
|
|
</div> |
|
</div> |
|
|
|
<script> |
|
let selectedText = ''; |
|
let selectedElement = null; |
|
let marginalia = []; |
|
let books = []; |
|
let searchInProgress = false; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
loadBooks(); |
|
initializeTextSelection(); |
|
testGradioConnection(); |
|
initializeTextArea(); |
|
|
|
|
|
document.addEventListener('click', function(e) { |
|
|
|
if (e.target.closest('#custom-text')) { |
|
return; |
|
} |
|
|
|
|
|
if (e.target.closest('.marginalia-marker') || e.target.closest('.marginalia-tooltip')) { |
|
return; |
|
} |
|
|
|
|
|
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => { |
|
tooltip.remove(); |
|
}); |
|
}); |
|
}); |
|
|
|
|
|
function initializeTextArea() { |
|
const textArea = document.getElementById('custom-text'); |
|
|
|
|
|
textArea.setAttribute('tabindex', '0'); |
|
|
|
|
|
textArea.addEventListener('keydown', function(e) { |
|
e.stopPropagation(); |
|
}); |
|
|
|
textArea.addEventListener('keyup', function(e) { |
|
e.stopPropagation(); |
|
}); |
|
|
|
textArea.addEventListener('input', function(e) { |
|
e.stopPropagation(); |
|
}); |
|
|
|
textArea.addEventListener('paste', function(e) { |
|
e.stopPropagation(); |
|
}); |
|
|
|
textArea.addEventListener('cut', function(e) { |
|
e.stopPropagation(); |
|
}); |
|
|
|
textArea.addEventListener('copy', function(e) { |
|
e.stopPropagation(); |
|
}); |
|
|
|
|
|
textArea.addEventListener('click', function(e) { |
|
e.stopPropagation(); |
|
this.focus(); |
|
}); |
|
} |
|
|
|
|
|
async function testGradioConnection() { |
|
const statusIndicator = document.getElementById('status-indicator'); |
|
const statusText = document.getElementById('status-text'); |
|
const statusDiv = document.getElementById('connection-status'); |
|
|
|
try { |
|
console.log('Testing connection to Gradio app...'); |
|
|
|
|
|
const spaceCheck = await fetch('https://medieval-data-latin-vulgate.hf.space/', { |
|
method: 'HEAD' |
|
}); |
|
console.log('Space accessibility check:', spaceCheck.status); |
|
|
|
|
|
console.log('Testing Gradio API with /gradio_api/call/predict endpoint...'); |
|
|
|
|
|
const postResponse = await fetch('https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
data: ["test", [], 1, "vector"] |
|
}) |
|
}); |
|
|
|
console.log('POST response status:', postResponse.status); |
|
|
|
if (!postResponse.ok) { |
|
throw new Error(`API endpoint not available (${postResponse.status})`); |
|
} |
|
|
|
const postResult = await postResponse.json(); |
|
console.log('POST response:', postResult); |
|
|
|
if (!postResult.event_id) { |
|
throw new Error('Invalid API response - no event_id received'); |
|
} |
|
|
|
console.log('Got event_id:', postResult.event_id); |
|
|
|
|
|
const getResponse = await fetch(`https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict/${postResult.event_id}`, { |
|
method: 'GET', |
|
}); |
|
|
|
console.log('GET response status:', getResponse.status); |
|
|
|
if (getResponse.ok) { |
|
statusIndicator.textContent = '✅'; |
|
statusText.textContent = 'Connected to search backend'; |
|
statusDiv.style.backgroundColor = '#d4edda'; |
|
statusDiv.style.color = '#155724'; |
|
statusDiv.style.border = '1px solid #c3e6cb'; |
|
console.log('API connection successful'); |
|
|
|
|
|
const apiNotice = document.getElementById('api-notice'); |
|
if (apiNotice) { |
|
apiNotice.style.display = 'none'; |
|
} |
|
} else { |
|
throw new Error(`GET endpoint failed (${getResponse.status})`); |
|
} |
|
|
|
} catch (error) { |
|
|
|
statusIndicator.textContent = '🔄'; |
|
statusText.textContent = 'API not ready - will retry automatically...'; |
|
statusDiv.style.backgroundColor = '#fff3cd'; |
|
statusDiv.style.color = '#856404'; |
|
statusDiv.style.border = '1px solid #ffeaa7'; |
|
|
|
console.error('Gradio app connection test failed:', error); |
|
|
|
setTimeout(() => { |
|
console.log('Retrying API connection...'); |
|
testGradioConnection(); |
|
}, 30000); |
|
} |
|
} |
|
|
|
|
|
async function loadBooks() { |
|
try { |
|
|
|
const bookList = [ |
|
"Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy", "Joshua", "Judges", "Ruth", |
|
"1 Samuel", "2 Samuel", "1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles", "Ezra", |
|
"Nehemiah", "Tobit", "Judith", "Esther", "1 Maccabees", "2 Maccabees", "Job", "Psalms", |
|
"Proverbs", "Ecclesiastes", "Song of Solomon", "Wisdom", "Sirach", "Isaiah", "Jeremiah", |
|
"Lamentations", "Baruch", "Ezekiel", "Daniel", "Hosea", "Joel", "Amos", "Obadiah", |
|
"Jonah", "Micah", "Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah", "Malachi", |
|
"Matthew", "Mark", "Luke", "John", "Acts", "Romans", "1 Corinthians", "2 Corinthians", |
|
"Galatians", "Ephesians", "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians", |
|
"1 Timothy", "2 Timothy", "Titus", "Philemon", "Hebrews", "James", "1 Peter", "2 Peter", |
|
"1 John", "2 John", "3 John", "Jude", "Revelation" |
|
]; |
|
|
|
books = bookList; |
|
const bookFilter = document.getElementById('book-filter'); |
|
books.forEach(book => { |
|
const option = document.createElement('option'); |
|
option.value = book; |
|
option.textContent = book; |
|
bookFilter.appendChild(option); |
|
}); |
|
} catch (error) { |
|
console.error('Error loading books:', error); |
|
} |
|
} |
|
|
|
|
|
function initializeTextSelection() { |
|
const textContent = document.getElementById('text-content'); |
|
|
|
textContent.addEventListener('mouseup', function(e) { |
|
const selection = window.getSelection(); |
|
if (selection.toString().trim().length > 0) { |
|
selectedText = selection.toString().trim(); |
|
searchInProgress = true; |
|
|
|
|
|
if (selection.rangeCount > 0) { |
|
const range = selection.getRangeAt(0); |
|
const span = document.createElement('span'); |
|
span.className = 'selected-text'; |
|
|
|
try { |
|
range.surroundContents(span); |
|
selectedElement = span; |
|
|
|
|
|
setTimeout(() => { |
|
if (selectedText && searchInProgress) { |
|
searchSimilarPassages(selectedText); |
|
} |
|
}, 100); |
|
} catch (error) { |
|
|
|
selection.removeAllRanges(); |
|
searchInProgress = false; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
|
|
document.addEventListener('click', function(e) { |
|
|
|
if (e.target.closest('#custom-text')) { |
|
return; |
|
} |
|
|
|
|
|
if (searchInProgress) { |
|
return; |
|
} |
|
|
|
|
|
if (e.target.closest('#search-popup') || e.target.closest('.popup-overlay')) { |
|
return; |
|
} |
|
|
|
|
|
if (e.target.closest('.marginalia-marker')) { |
|
return; |
|
} |
|
|
|
|
|
if (e.target.closest('#text-content') && window.getSelection().toString().trim().length > 0) { |
|
return; |
|
} |
|
|
|
|
|
if (!e.target.closest('#text-content')) { |
|
clearTemporarySelection(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function clearTemporarySelection() { |
|
const tempSelected = document.querySelector('.selected-text'); |
|
if (tempSelected && !tempSelected.classList.contains('marginalia-marker')) { |
|
const parent = tempSelected.parentNode; |
|
while (tempSelected.firstChild) { |
|
parent.insertBefore(tempSelected.firstChild, tempSelected); |
|
} |
|
parent.removeChild(tempSelected); |
|
} |
|
|
|
|
|
selectedText = ''; |
|
selectedElement = null; |
|
searchInProgress = false; |
|
window.getSelection().removeAllRanges(); |
|
} |
|
|
|
|
|
function parseGradioResults(htmlResult, query) { |
|
try { |
|
|
|
const tempDiv = document.createElement('div'); |
|
tempDiv.innerHTML = htmlResult; |
|
|
|
|
|
const table = tempDiv.querySelector('table'); |
|
if (!table) { |
|
return []; |
|
} |
|
|
|
const rows = table.querySelectorAll('tbody tr'); |
|
const results = []; |
|
|
|
rows.forEach(row => { |
|
const cells = row.querySelectorAll('td'); |
|
if (cells.length >= 6) { |
|
results.push({ |
|
reference: cells[0].textContent.trim(), |
|
text: cells[1].innerHTML, |
|
similarity: parseFloat(cells[2].textContent.trim()), |
|
book: cells[3].textContent.trim(), |
|
chapter: parseInt(cells[4].textContent.trim()), |
|
verse: parseInt(cells[5].textContent.trim()), |
|
raw_text: cells[1].textContent.trim() |
|
}); |
|
} |
|
}); |
|
|
|
return results; |
|
} catch (error) { |
|
console.error('Error parsing Gradio results:', error); |
|
return []; |
|
} |
|
} |
|
|
|
|
|
async function searchSimilarPassages(query) { |
|
const popup = document.getElementById('search-popup'); |
|
const overlay = document.getElementById('popup-overlay'); |
|
const results = document.getElementById('search-results'); |
|
const queryTextDiv = document.getElementById('query-text'); |
|
|
|
|
|
results.innerHTML = '<div class="loading">Searching for similar passages...</div>'; |
|
popup.style.display = 'block'; |
|
overlay.style.display = 'block'; |
|
|
|
|
|
searchInProgress = false; |
|
|
|
|
|
queryTextDiv.textContent = `Query: "${query}"`; |
|
|
|
try { |
|
const searchMethod = document.getElementById('search-method').value; |
|
const resultLimit = parseInt(document.getElementById('result-limit').value); |
|
const selectedBooks = Array.from(document.getElementById('book-filter').selectedOptions) |
|
.map(option => option.value); |
|
|
|
console.log(`Searching with query: "${query}"`); |
|
|
|
|
|
const postResponse = await fetch('https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
data: [query, selectedBooks, resultLimit, searchMethod] |
|
}) |
|
}); |
|
|
|
if (!postResponse.ok) { |
|
throw new Error(`POST request failed: ${postResponse.status} ${postResponse.statusText}`); |
|
} |
|
|
|
const postResult = await postResponse.json(); |
|
console.log('POST response:', postResult); |
|
|
|
if (!postResult.event_id) { |
|
throw new Error('No event_id received from server'); |
|
} |
|
|
|
const eventId = postResult.event_id; |
|
console.log('Got event_id:', eventId); |
|
|
|
|
|
const getResponse = await fetch(`https://medieval-data-latin-vulgate.hf.space/gradio_api/call/predict/${eventId}`, { |
|
method: 'GET', |
|
}); |
|
|
|
if (!getResponse.ok) { |
|
throw new Error(`GET request failed: ${getResponse.status} ${getResponse.statusText}`); |
|
} |
|
|
|
|
|
const reader = getResponse.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
let finalResult = null; |
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
|
|
if (done) break; |
|
|
|
buffer += decoder.decode(value, { stream: true }); |
|
const lines = buffer.split('\n'); |
|
buffer = lines.pop() || ''; |
|
|
|
for (const line of lines) { |
|
if (line.startsWith('data: ')) { |
|
try { |
|
const data = JSON.parse(line.slice(6)); |
|
console.log('Received data:', data); |
|
if (Array.isArray(data) && data.length > 0) { |
|
finalResult = data[0]; |
|
} |
|
} catch (error) { |
|
console.log('Error parsing data line:', error); |
|
} |
|
} else if (line.startsWith('event: ')) { |
|
const eventType = line.slice(8); |
|
console.log('Event type:', eventType); |
|
|
|
if (eventType === 'complete' && finalResult) { |
|
|
|
const searchResults = parseGradioResults(finalResult, query); |
|
displaySearchResults(searchResults, query); |
|
return; |
|
} else if (eventType === 'error') { |
|
throw new Error('Server returned error event'); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (finalResult) { |
|
const searchResults = parseGradioResults(finalResult, query); |
|
displaySearchResults(searchResults, query); |
|
} else { |
|
throw new Error('No results received from server'); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Error searching:', error); |
|
results.innerHTML = ` |
|
<div class="error"> |
|
<strong>Search Error:</strong> ${error.message}<br><br> |
|
<strong>Troubleshooting:</strong><br> |
|
1. Check if your <a href="https://huggingface.co/spaces/medieval-data/latin-vulgate" target="_blank">HuggingFace Space</a> is running<br> |
|
2. Try a "Factory restart" in Space settings<br> |
|
3. Verify the Space has <code>api_name="predict"</code> in the Gradio interface<br> |
|
4. Wait 2-3 minutes after restart for API to become available |
|
</div> |
|
`; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function displaySearchResults(results, originalQuery) { |
|
const resultsContainer = document.getElementById('search-results'); |
|
const queryTextDiv = document.getElementById('query-text'); |
|
|
|
|
|
queryTextDiv.innerHTML = `<strong>Query:</strong> "${originalQuery}"`; |
|
|
|
if (results.length === 0) { |
|
resultsContainer.innerHTML = '<div class="loading">No similar passages found.</div>'; |
|
return; |
|
} |
|
|
|
let html = ''; |
|
results.forEach((result, index) => { |
|
const resultId = `result-${Date.now()}-${index}`; |
|
html += ` |
|
<div class="result-item" data-result-id="${resultId}" data-original-query="${originalQuery}"> |
|
<div class="result-reference">${result.reference}</div> |
|
<div class="result-text">${result.text}</div> |
|
<div class="result-similarity">Similarity: ${result.similarity}</div> |
|
</div> |
|
`; |
|
|
|
|
|
window.searchResults = window.searchResults || {}; |
|
window.searchResults[resultId] = { |
|
reference: result.reference, |
|
text: result.text, |
|
similarity: result.similarity, |
|
raw_text: result.raw_text || result.text |
|
}; |
|
}); |
|
|
|
resultsContainer.innerHTML = html; |
|
|
|
|
|
resultsContainer.querySelectorAll('.result-item').forEach(item => { |
|
item.addEventListener('click', function() { |
|
const resultId = this.getAttribute('data-result-id'); |
|
const originalQuery = this.getAttribute('data-original-query'); |
|
const resultData = window.searchResults[resultId]; |
|
|
|
if (resultData) { |
|
addMarginalia(originalQuery, resultData); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function addMarginalia(originalText, result) { |
|
const marginaliaId = Date.now().toString(); |
|
|
|
|
|
marginalia.push({ |
|
id: marginaliaId, |
|
originalText: originalText, |
|
reference: result.reference, |
|
text: result.raw_text, |
|
similarity: result.similarity |
|
}); |
|
|
|
|
|
if (selectedElement) { |
|
selectedElement.className = 'marginalia-marker'; |
|
selectedElement.setAttribute('data-marginalia-id', marginaliaId); |
|
|
|
|
|
updateMarginaliaIndicator(selectedElement); |
|
|
|
|
|
setupMarginaliaHover(selectedElement); |
|
} |
|
|
|
|
|
addMarginaliaToMargin(marginaliaId); |
|
|
|
|
|
closePopup(); |
|
|
|
|
|
selectedText = ''; |
|
selectedElement = null; |
|
window.getSelection().removeAllRanges(); |
|
} |
|
|
|
|
|
function addMarginaliaToMargin(marginaliaId) { |
|
const marginList = document.getElementById('marginalia-list'); |
|
const marginaliaItem = marginalia.find(item => item.id === marginaliaId); |
|
|
|
if (!marginaliaItem) return; |
|
|
|
|
|
const placeholder = marginList.querySelector('p'); |
|
if (placeholder) { |
|
placeholder.remove(); |
|
} |
|
|
|
|
|
const marginNote = document.createElement('div'); |
|
marginNote.className = 'margin-note'; |
|
marginNote.setAttribute('data-marginalia-id', marginaliaId); |
|
|
|
|
|
const truncatedText = truncateText(marginaliaItem.text, 120); |
|
|
|
marginNote.innerHTML = ` |
|
<div class="margin-note-reference">${marginaliaItem.reference}</div> |
|
<div class="margin-note-text" title="${marginaliaItem.text}">${truncatedText}</div> |
|
<div class="margin-note-similarity">Similarity: ${marginaliaItem.similarity}</div> |
|
<button class="margin-note-remove" onclick="removeMarginalia('${marginaliaId}')" title="Remove marginalia">×</button> |
|
`; |
|
|
|
|
|
marginNote.addEventListener('mouseenter', function() { |
|
highlightCorrespondingText(marginaliaId, true); |
|
}); |
|
|
|
marginNote.addEventListener('mouseleave', function() { |
|
highlightCorrespondingText(marginaliaId, false); |
|
}); |
|
|
|
|
|
marginNote.addEventListener('click', function() { |
|
scrollToText(marginaliaId); |
|
}); |
|
|
|
marginList.appendChild(marginNote); |
|
} |
|
|
|
|
|
function highlightCorrespondingText(marginaliaId, highlight) { |
|
const textMarker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`); |
|
const marginNote = document.querySelector(`.margin-note[data-marginalia-id="${marginaliaId}"]`); |
|
|
|
if (textMarker) { |
|
if (highlight) { |
|
textMarker.style.backgroundColor = '#ffffcc'; |
|
textMarker.style.outline = '2px solid #3498db'; |
|
} else { |
|
textMarker.style.backgroundColor = '#e8f4fd'; |
|
textMarker.style.outline = ''; |
|
} |
|
} |
|
|
|
if (marginNote) { |
|
if (highlight) { |
|
marginNote.classList.add('highlighted'); |
|
} else { |
|
marginNote.classList.remove('highlighted'); |
|
} |
|
} |
|
} |
|
|
|
|
|
function scrollToText(marginaliaId) { |
|
const textMarker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`); |
|
if (textMarker) { |
|
textMarker.scrollIntoView({ |
|
behavior: 'smooth', |
|
block: 'center' |
|
}); |
|
|
|
|
|
highlightCorrespondingText(marginaliaId, true); |
|
setTimeout(() => { |
|
highlightCorrespondingText(marginaliaId, false); |
|
}, 2000); |
|
} |
|
} |
|
|
|
|
|
function updateMarginaliaIndicator(element) { |
|
|
|
const existingIndicator = element.querySelector('.marginalia-indicator'); |
|
if (existingIndicator) { |
|
existingIndicator.remove(); |
|
} |
|
|
|
|
|
const elementMarginalia = marginalia.filter(item => |
|
element.getAttribute('data-marginalia-id') === item.id || |
|
element.getAttribute('data-marginalia-ids')?.split(',').includes(item.id) |
|
); |
|
|
|
|
|
const allIds = element.getAttribute('data-marginalia-id') ? |
|
[element.getAttribute('data-marginalia-id')] : []; |
|
|
|
if (elementMarginalia.length > 0) { |
|
const indicator = document.createElement('div'); |
|
indicator.className = elementMarginalia.length > 1 ? |
|
'marginalia-indicator multiple' : 'marginalia-indicator'; |
|
indicator.textContent = elementMarginalia.length > 1 ? |
|
elementMarginalia.length.toString() : '•'; |
|
element.appendChild(indicator); |
|
|
|
|
|
if (elementMarginalia.length > 1) { |
|
element.setAttribute('data-marginalia-ids', |
|
elementMarginalia.map(m => m.id).join(',')); |
|
} |
|
} |
|
} |
|
|
|
|
|
function setupMarginaliaHover(element) { |
|
|
|
element.removeEventListener('mouseenter', showMarginaliaTooltip); |
|
element.removeEventListener('mouseleave', hideMarginaliaTooltip); |
|
element.removeEventListener('click', showMarginaliaTooltip); |
|
|
|
|
|
element.addEventListener('mouseenter', function(e) { |
|
showMarginaliaTooltip(e); |
|
|
|
const marginaliaId = element.getAttribute('data-marginalia-id'); |
|
if (marginaliaId) { |
|
highlightCorrespondingText(marginaliaId, true); |
|
} |
|
}); |
|
|
|
element.addEventListener('mouseleave', function(e) { |
|
hideMarginaliaTooltip(e); |
|
|
|
const marginaliaId = element.getAttribute('data-marginalia-id'); |
|
if (marginaliaId) { |
|
highlightCorrespondingText(marginaliaId, false); |
|
} |
|
}); |
|
|
|
element.addEventListener('click', function(e) { |
|
e.stopPropagation(); |
|
showMarginaliaTooltip(e); |
|
}); |
|
} |
|
|
|
|
|
function showMarginaliaTooltip(event) { |
|
const element = event.target.closest('.marginalia-marker'); |
|
if (!element) return; |
|
|
|
const marginaliaId = element.getAttribute('data-marginalia-id'); |
|
const marginaliaIds = element.getAttribute('data-marginalia-ids'); |
|
|
|
console.log('Showing tooltip for element:', element, 'ID:', marginaliaId, 'IDs:', marginaliaIds); |
|
|
|
let relevantMarginalia = []; |
|
|
|
if (marginaliaIds) { |
|
|
|
const ids = marginaliaIds.split(','); |
|
relevantMarginalia = marginalia.filter(item => ids.includes(item.id)); |
|
} else if (marginaliaId) { |
|
|
|
relevantMarginalia = marginalia.filter(item => item.id === marginaliaId); |
|
} |
|
|
|
console.log('Relevant marginalia:', relevantMarginalia); |
|
|
|
if (relevantMarginalia.length === 0) return; |
|
|
|
|
|
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => { |
|
tooltip.remove(); |
|
}); |
|
|
|
|
|
const tooltip = document.createElement('div'); |
|
tooltip.className = 'marginalia-tooltip show'; |
|
|
|
let tooltipHTML = ''; |
|
relevantMarginalia.forEach(item => { |
|
tooltipHTML += ` |
|
<div class="marginalia-tooltip-item"> |
|
<div class="marginalia-tooltip-reference">${item.reference}</div> |
|
<div class="marginalia-tooltip-text">${item.text}</div> |
|
<div class="marginalia-tooltip-similarity">Similarity: ${item.similarity}</div> |
|
</div> |
|
`; |
|
}); |
|
|
|
tooltip.innerHTML = tooltipHTML; |
|
|
|
|
|
document.body.appendChild(tooltip); |
|
|
|
|
|
const rect = element.getBoundingClientRect(); |
|
const tooltipRect = tooltip.getBoundingClientRect(); |
|
|
|
let left = rect.left; |
|
let top = rect.bottom + window.scrollY + 5; |
|
|
|
|
|
if (left + tooltipRect.width > window.innerWidth) { |
|
left = window.innerWidth - tooltipRect.width - 10; |
|
} |
|
|
|
if (top + tooltipRect.height > window.innerHeight + window.scrollY) { |
|
top = rect.top + window.scrollY - tooltipRect.height - 5; |
|
} |
|
|
|
tooltip.style.position = 'absolute'; |
|
tooltip.style.left = left + 'px'; |
|
tooltip.style.top = top + 'px'; |
|
tooltip.style.zIndex = '9999'; |
|
|
|
console.log('Tooltip positioned at:', left, top); |
|
|
|
|
|
element._tooltip = tooltip; |
|
} |
|
|
|
|
|
function hideMarginaliaTooltip(event) { |
|
const element = event.target.closest('.marginalia-marker'); |
|
if (!element) return; |
|
|
|
|
|
setTimeout(() => { |
|
if (element._tooltip) { |
|
element._tooltip.remove(); |
|
element._tooltip = null; |
|
} |
|
}, 100); |
|
} |
|
|
|
|
|
|
|
|
|
function removeMarginalia(marginaliaId) { |
|
console.log('Removing marginalia:', marginaliaId); |
|
|
|
|
|
marginalia = marginalia.filter(item => item.id !== marginaliaId); |
|
|
|
|
|
const marginNote = document.querySelector(`.margin-note[data-marginalia-id="${marginaliaId}"]`); |
|
if (marginNote) { |
|
marginNote.remove(); |
|
} |
|
|
|
|
|
const marginList = document.getElementById('marginalia-list'); |
|
if (marginalia.length === 0) { |
|
marginList.innerHTML = '<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>'; |
|
} |
|
|
|
|
|
const marker = document.querySelector(`[data-marginalia-id="${marginaliaId}"]`) || |
|
document.querySelector(`[data-marginalia-ids*="${marginaliaId}"]`); |
|
|
|
if (marker) { |
|
|
|
const marginaliaIds = marker.getAttribute('data-marginalia-ids'); |
|
|
|
if (marginaliaIds) { |
|
|
|
const remainingIds = marginaliaIds.split(',').filter(id => id !== marginaliaId); |
|
|
|
if (remainingIds.length > 1) { |
|
|
|
marker.setAttribute('data-marginalia-ids', remainingIds.join(',')); |
|
updateMarginaliaIndicator(marker); |
|
} else if (remainingIds.length === 1) { |
|
|
|
marker.removeAttribute('data-marginalia-ids'); |
|
marker.setAttribute('data-marginalia-id', remainingIds[0]); |
|
updateMarginaliaIndicator(marker); |
|
} else { |
|
|
|
const parent = marker.parentNode; |
|
while (marker.firstChild) { |
|
parent.insertBefore(marker.firstChild, marker); |
|
} |
|
parent.removeChild(marker); |
|
} |
|
} else { |
|
|
|
const parent = marker.parentNode; |
|
while (marker.firstChild) { |
|
parent.insertBefore(marker.firstChild, marker); |
|
} |
|
parent.removeChild(marker); |
|
} |
|
|
|
|
|
if (marker._tooltip) { |
|
marker._tooltip.remove(); |
|
marker._tooltip = null; |
|
} |
|
} |
|
|
|
|
|
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => { |
|
tooltip.remove(); |
|
}); |
|
} |
|
|
|
|
|
function closePopup() { |
|
document.getElementById('search-popup').style.display = 'none'; |
|
document.getElementById('popup-overlay').style.display = 'none'; |
|
|
|
clearTemporarySelection(); |
|
} |
|
|
|
|
|
function clearSelection() { |
|
clearTemporarySelection(); |
|
} |
|
|
|
|
|
function exportToTEI() { |
|
const textContent = document.getElementById('text-content').cloneNode(true); |
|
|
|
|
|
const controls = textContent.querySelector('.controls'); |
|
if (controls) controls.remove(); |
|
|
|
|
|
const cleanHtml = textContent.innerHTML; |
|
|
|
|
|
const teiXml = generateTEIXML(cleanHtml); |
|
|
|
|
|
const blob = new Blob([teiXml], { type: 'application/xml' }); |
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
a.href = url; |
|
a.download = 'vulgate-marginalia.xml'; |
|
document.body.appendChild(a); |
|
a.click(); |
|
document.body.removeChild(a); |
|
URL.revokeObjectURL(url); |
|
} |
|
|
|
|
|
function generateTEIXML(htmlContent) { |
|
const timestamp = new Date().toISOString(); |
|
|
|
console.log('Original HTML content:', htmlContent); |
|
|
|
|
|
const tempDiv = document.createElement('div'); |
|
tempDiv.innerHTML = htmlContent; |
|
|
|
|
|
const controls = tempDiv.querySelector('.controls'); |
|
if (controls) controls.remove(); |
|
|
|
console.log('HTML after removing controls:', tempDiv.innerHTML); |
|
|
|
|
|
const cleanText = cleanHtmlToTei(tempDiv.innerHTML); |
|
|
|
console.log('Cleaned TEI text:', cleanText); |
|
console.log('Current marginalia array:', marginalia); |
|
|
|
let teiHeader = `<?xml version="1.0" encoding="UTF-8"?> |
|
<TEI xmlns="http://www.tei-c.org/ns/1.0"> |
|
<teiHeader> |
|
<fileDesc> |
|
<titleStmt> |
|
<title>Latin Vulgate Text with Marginalia</title> |
|
<author>Generated by Vulgate Text Editor</author> |
|
</titleStmt> |
|
<publicationStmt> |
|
<p>Generated on ${timestamp}</p> |
|
</publicationStmt> |
|
<sourceDesc> |
|
<p>Latin Vulgate text with semantic similarity annotations</p> |
|
</sourceDesc> |
|
</fileDesc> |
|
</teiHeader> |
|
<text> |
|
<body> |
|
${cleanText}`; |
|
|
|
|
|
if (marginalia.length > 0) { |
|
teiHeader += '\n <div type="apparatus">\n'; |
|
marginalia.forEach(item => { |
|
|
|
const refParts = parseReference(item.reference); |
|
|
|
teiHeader += ` <note xml:id="${item.id}" type="parallel" resp="#semantic-analysis"> |
|
<bibl> |
|
<title type="biblical-book">${refParts.book}</title> |
|
<biblScope unit="chapter">${refParts.chapter}</biblScope> |
|
<biblScope unit="verse">${refParts.verse}</biblScope> |
|
</bibl> |
|
<quote xml:lang="la">${escapeXml(item.text)}</quote> |
|
<measure type="similarity" quantity="${item.similarity}"/> |
|
</note>\n`; |
|
}); |
|
teiHeader += ' </div>\n'; |
|
} |
|
|
|
const teiFooter = ` </body> |
|
</text> |
|
</TEI>`; |
|
|
|
const finalXml = teiHeader + teiFooter; |
|
console.log('Final XML:', finalXml); |
|
|
|
return finalXml; |
|
} |
|
|
|
|
|
function cleanHtmlToTei(htmlContent) { |
|
let teiContent = htmlContent; |
|
|
|
console.log('Starting cleanHtmlToTei with:', teiContent); |
|
|
|
|
|
teiContent = teiContent.replace(/<h2[^>]*>(.*?)<\/h2>/g, ' <head>$1</head>'); |
|
|
|
|
|
|
|
let replacements = 0; |
|
teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*data-marginalia-id="([^"]*)"[^>]*>(.*?)<div[^>]*class="marginalia-indicator"[^>]*>.*?<\/div><\/span>/gs, |
|
(match, id, text) => { |
|
console.log(`Replacing marginalia marker ${id} with text: "${text}"`); |
|
replacements++; |
|
return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`; |
|
}); |
|
|
|
|
|
teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*data-marginalia-id="([^"]*)"[^>]*>(.*?)<\/span>/gs, |
|
(match, id, text) => { |
|
console.log(`Replacing simple marginalia marker ${id} with text: "${text}"`); |
|
replacements++; |
|
return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`; |
|
}); |
|
|
|
|
|
teiContent = teiContent.replace(/<span[^>]*data-marginalia-id="([^"]*)"[^>]*class="marginalia-marker"[^>]*>(.*?)<\/span>/gs, |
|
(match, id, text) => { |
|
console.log(`Replacing alt-order marginalia marker ${id} with text: "${text}"`); |
|
replacements++; |
|
return `<seg xml:id="seg_${id}" corresp="#${id}">${text}</seg>`; |
|
}); |
|
|
|
console.log(`Made ${replacements} marginalia replacements`); |
|
|
|
|
|
teiContent = teiContent.replace(/<span[^>]*class="marginalia-marker"[^>]*>/g, '<seg>'); |
|
teiContent = teiContent.replace(/<\/span>/g, '</seg>'); |
|
teiContent = teiContent.replace(/<div[^>]*class="marginalia-indicator"[^>]*>.*?<\/div>/gs, ''); |
|
|
|
|
|
teiContent = teiContent.replace(/style="[^"]*"/g, ''); |
|
teiContent = teiContent.replace(/class="[^"]*"/g, ''); |
|
teiContent = teiContent.replace(/data-[^=]*="[^"]*"/g, ''); |
|
|
|
|
|
teiContent = teiContent.replace(/<p>/g, ' <p>'); |
|
teiContent = teiContent.replace(/<\/p>/g, '</p>'); |
|
|
|
|
|
teiContent = teiContent.replace(/\s*<p>\s*<\/p>\s*/g, ''); |
|
|
|
|
|
teiContent = teiContent.replace(/\s+>/g, '>'); |
|
teiContent = teiContent.replace(/>\s+</g, '><'); |
|
|
|
|
|
teiContent = teiContent.replace(/(<\/p>)(?=\s*<)/g, '$1\n'); |
|
teiContent = teiContent.replace(/(<\/head>)(?=\s*<)/g, '$1\n'); |
|
|
|
|
|
teiContent = teiContent.trim(); |
|
|
|
console.log('Final cleaned TEI content:', teiContent); |
|
|
|
return teiContent; |
|
} |
|
|
|
|
|
function parseReference(reference) { |
|
|
|
const match = reference.match(/^(\d?\s*\w+)\s+(\d+):(\d+)(?:-\d+)?$/); |
|
|
|
if (match) { |
|
return { |
|
book: match[1].trim(), |
|
chapter: match[2], |
|
verse: match[3] |
|
}; |
|
} |
|
|
|
|
|
return { |
|
book: reference, |
|
chapter: "1", |
|
verse: "1" |
|
}; |
|
} |
|
|
|
|
|
function escapeXml(text) { |
|
return text |
|
.replace(/&/g, '&') |
|
.replace(/</g, '<') |
|
.replace(/>/g, '>') |
|
.replace(/"/g, '"') |
|
.replace(/'/g, '''); |
|
} |
|
|
|
|
|
function truncateText(text, maxLength) { |
|
if (text.length <= maxLength) return text; |
|
|
|
|
|
const truncated = text.substring(0, maxLength); |
|
const lastSpace = truncated.lastIndexOf(' '); |
|
|
|
if (lastSpace > maxLength * 0.8) { |
|
return truncated.substring(0, lastSpace) + '...'; |
|
} |
|
|
|
return truncated + '...'; |
|
} |
|
|
|
|
|
function loadCustomText() { |
|
const customTextArea = document.getElementById('custom-text'); |
|
const textContent = document.getElementById('text-content'); |
|
|
|
if (customTextArea.value.trim()) { |
|
const inputText = customTextArea.value.trim(); |
|
|
|
|
|
clearAllMarginalia(); |
|
|
|
|
|
let formattedText = ''; |
|
|
|
|
|
if (inputText.includes('<') && inputText.includes('>')) { |
|
|
|
formattedText = inputText; |
|
} else { |
|
|
|
|
|
const paragraphs = inputText.split(/\n\s*\n/); |
|
|
|
|
|
if (!inputText.toLowerCase().includes('<h') && paragraphs.length > 0) { |
|
formattedText = '<h2>Custom Text</h2>\n'; |
|
} |
|
|
|
|
|
paragraphs.forEach(para => { |
|
const cleanPara = para.replace(/\n/g, ' ').trim(); |
|
if (cleanPara) { |
|
formattedText += `<p>${cleanPara}</p>\n`; |
|
} |
|
}); |
|
} |
|
|
|
textContent.innerHTML = formattedText; |
|
|
|
|
|
customTextArea.value = ''; |
|
|
|
|
|
initializeTextSelection(); |
|
|
|
|
|
showStatusMessage('Custom text loaded successfully!', 'success'); |
|
|
|
} else { |
|
showStatusMessage('Please paste your text into the textarea first.', 'error'); |
|
} |
|
} |
|
|
|
|
|
function clearAllMarginalia() { |
|
marginalia = []; |
|
const marginList = document.getElementById('marginalia-list'); |
|
marginList.innerHTML = '<p style="color: #999; font-style: italic; font-size: 0.85em;">Select text and add similar passages to see marginalia here.</p>'; |
|
|
|
|
|
document.querySelectorAll('.marginalia-marker').forEach(marker => { |
|
const parent = marker.parentNode; |
|
while (marker.firstChild) { |
|
parent.insertBefore(marker.firstChild, marker); |
|
} |
|
parent.removeChild(marker); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.marginalia-tooltip').forEach(tooltip => { |
|
tooltip.remove(); |
|
}); |
|
} |
|
|
|
|
|
function showStatusMessage(message, type = 'info') { |
|
const existingMessage = document.getElementById('status-message'); |
|
if (existingMessage) { |
|
existingMessage.remove(); |
|
} |
|
|
|
const messageDiv = document.createElement('div'); |
|
messageDiv.id = 'status-message'; |
|
messageDiv.style.cssText = ` |
|
position: fixed; |
|
top: 20px; |
|
right: 20px; |
|
padding: 12px 20px; |
|
border-radius: 4px; |
|
color: white; |
|
font-weight: bold; |
|
z-index: 10000; |
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2); |
|
${type === 'success' ? 'background: #27ae60;' : ''} |
|
${type === 'error' ? 'background: #e74c3c;' : ''} |
|
${type === 'info' ? 'background: #3498db;' : ''} |
|
`; |
|
messageDiv.textContent = message; |
|
|
|
document.body.appendChild(messageDiv); |
|
|
|
|
|
setTimeout(() => { |
|
if (messageDiv.parentNode) { |
|
messageDiv.remove(); |
|
} |
|
}, 3000); |
|
} |
|
|
|
|
|
function resetToSample() { |
|
const textContent = document.getElementById('text-content'); |
|
textContent.innerHTML = ` |
|
<h2>De Imitatione Christi - Sample Text</h2> |
|
<p>Qui sequitur me, non ambulat in tenebris, dicit Dominus. Haec sunt verba Christi, quibus admonemur, quatenus vitam ejus et mores imitemur, si volumus veraciter illuminari, et ab omni caecitate cordis liberari. Summum igitur studium nostrum sit, in vita Jesu Christi meditari.</p> |
|
|
|
<p>Doctrina Christi omnes doctrinas sanctorum praecellit, et qui spiritum habet, inveniet ibi manna absconditum. Sed contingit, quod multi ex frequenti auditione Evangelii, parum desiderium sentiunt: quia spiritum Christi non habent. Qui autem vult plene et sapide verba Christi intelligere, oportet ut totam vitam suam illi studeat conformare.</p> |
|
|
|
<p>Quid tibi prodest alta de Trinitate disputare, si cares humilitate, unde displiceas Trinitati? Vere alta verba non faciunt sanctum et justum; sed virtuosa vita efficit Deo carum. Opto magis sentire compunctionem, quam scire ejus definitionem.</p> |
|
|
|
<p>Si scires totam Bibliam exterius, et omnium philosophorum dicta, quid totum prodesset sine caritate et gratia Dei? Vanitas vanitatum, et omnia vanitas, praeter amare Deum, et illi soli servire. Haec est summa sapientia: per contemptum mundi tendere ad regna caelestia.</p> |
|
|
|
<p>Vanitas igitur est, honores perishables sectari, et ad alta loca ascendere. Vanitas est, carnis desideria sequi, et illud desiderare unde oporteat postea gravius puniri. Vanitas est, longam vitam optare, et de bona vita parum curare. Vanitas est, praesentem vitam tantum attendere, et quae futura sunt non prospicere.</p> |
|
`; |
|
|
|
clearAllMarginalia(); |
|
|
|
|
|
initializeTextSelection(); |
|
|
|
|
|
showStatusMessage('Reset to sample text successfully!', 'success'); |
|
} |
|
|
|
|
|
function clearTextArea() { |
|
const customTextArea = document.getElementById('custom-text'); |
|
customTextArea.value = ''; |
|
showStatusMessage('Custom text area cleared.', 'info'); |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
|