Skip to content

Instantly share code, notes, and snippets.

@avimar
Created June 15, 2025 09:52
Show Gist options
  • Save avimar/868c259c0476f59a93a1cbaa2b62ff1c to your computer and use it in GitHub Desktop.
Save avimar/868c259c0476f59a93a1cbaa2b62ff1c to your computer and use it in GitHub Desktop.
Interlinear
<!DOCTYPE html>
<html lang="he">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hebrew Interlinear Viewer</title>
<style>
.interlinear-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
line-height: 1.1;
margin: 10px 0;
direction: rtl;
}
.word-group {
display: flex;
flex-direction: column;
align-items: center;
min-width: fit-content;
direction: ltr;
}
.hebrew {
font-family: 'SBL Hebrew', 'David', 'Times New Roman', serif;
font-size: 18px;
font-weight: bold;
text-align: center;
padding: 3px 6px;
border-bottom: 0.5px solid #ccc;
margin-bottom: 2px;
direction: rtl;
}
.header-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
margin: 20px 0;
text-align: center;
gap: 15px;
}
.header-container .hebrew {
font-size: 32px;
border-bottom: none;
margin-bottom: 0;
}
.header-container .english {
font-size: 24px;
color: #000;
}
.hebrew.big {
font-size: 24px;
font-weight: 900;
}
.hebrew.small {
font-size: 14px;
}
.word-group.big .english {
font-size: 12px;
}
.word-group.small .english {
font-size: 8px;
}
.english {
font-family: Arial, sans-serif;
font-size: 10px;
color: #222;
text-align: center;
padding: 2px 4px;
word-break: keep-all;
white-space: nowrap;
overflow-wrap: break-word;
direction: ltr;
}
.sentence-break-alt {
display: flex;
flex-direction: column;
align-items: center;
direction: ltr;
width: 5px;
}
.sentence-break-alt .hebrew-space {
font-size: 18px;
padding: 3px 6px;
margin-bottom: 2px;
border-bottom: 0.5px solid transparent;
position: relative;
}
.sentence-break-alt .hebrew-space::after {
content: "◆";
color: #0;
font-size: 10px;
position: absolute;
bottom: -1px;
left: 50%;
transform: translateX(-50%);
padding-left: 5px
}
.sentence-break-alt .english-space {
font-size: 12px;
padding: 2px 4px;
color: transparent;
}
.english.wrap-ok {
white-space: normal;
max-width: 100px;
}
.verse-break {
width: 100%;
height: 5px;
}
.paragraph-break {
width: 100%;
height: 25px;
}
body {
max-width: 900px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
color: #333;
border-bottom: 2px solid #ddd;
padding-bottom: 10px;
}
#input-form {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
#input-form input[type="text"] {
width: 100%;
padding: 8px;
margin: 8px 0;
}
#input-form button {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#input-form button:hover {
background: #45a049;
}
.error {
color: red;
margin: 10px 0;
}
#back-button {
position: fixed;
top: 10px;
left: 10px;
padding: 8px 16px;
background: #666;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
display: none;
}
#back-button:hover {
background: #555;
}
@media print {
#back-button {
display: none !important;
}
}
</style>
</head>
<body>
<h1>Hebrew Interlinear Viewer</h1>
<div id="input-form">
<input type="text" id="sheets-url" placeholder="Enter Google Sheets URL">
<button onclick="loadSheet()">Load Sheet</button>
<button onclick="loadDemo()" style="background: #666;">Demo: Hakaras HaTov</button>
<div id="error-message" class="error"></div>
</div>
<button id="back-button" onclick="resetView()">Back</button>
<div id="content" class="interlinear-container"></div>
<script>
// Load demo content
function loadDemo() {
document.getElementById('sheets-url').value = 'https://docs.google.com/spreadsheets/d/1TcL04FE0TU3diSOkRY7DZ25ab7DtaLdNkOnWOrIXqss';
loadSheet();
}
// Parse Google Sheets URL to get the ID
function getSheetId(url) {
// Handle regular Google Sheets URLs
let match = url.match(/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
if (match) return match[1];
throw new Error('Invalid Google Sheets URL. Please use a standard Google Sheets URL.');
}
// Create the interlinear display
function createInterlinear(data) {
const content = document.getElementById('content');
content.innerHTML = '';
let inParagraph = false;
data.forEach(row => {
if (row.type === 'break') {
if (inParagraph) {
content.innerHTML += `<div class="paragraph-break"></div>`;
inParagraph = false;
}
} else {
if (!inParagraph) {
inParagraph = true;
}
if (row.type === 'header') {
const headerContainer = document.createElement('div');
headerContainer.className = 'header-container';
const hebrew = document.createElement('div');
hebrew.className = 'hebrew';
hebrew.textContent = row.hebrew;
const english = document.createElement('div');
english.className = 'english';
english.textContent = row.english;
headerContainer.appendChild(hebrew);
const dash = document.createElement('div');
dash.className = 'english';
dash.style.fontSize = '24px';
dash.textContent = ' - ';
headerContainer.appendChild(dash);
headerContainer.appendChild(english);
content.appendChild(headerContainer);
} else {
const wordGroup = document.createElement('div');
wordGroup.className = 'word-group';
if (['big', 'small'].includes(row.type)) {
wordGroup.classList.add(row.type);
}
const hebrew = document.createElement('div');
hebrew.className = 'hebrew';
if (['big', 'small'].includes(row.type)) {
hebrew.classList.add(row.type);
}
hebrew.textContent = row.hebrew;
const english = document.createElement('div');
english.className = 'english';
english.textContent = row.english;
wordGroup.appendChild(hebrew);
wordGroup.appendChild(english);
content.appendChild(wordGroup);
}
if (row.hebrew.endsWith('.') && row.english.endsWith('.')) {
const breakDiv = document.createElement('div');
breakDiv.className = 'sentence-break-alt';
breakDiv.innerHTML = `
<div class="hebrew-space">&nbsp;</div>
<div class="english-space">&nbsp;</div>
`;
content.appendChild(breakDiv);
}
}
});
}
// Reset the view to initial state
function resetView() {
document.getElementById('input-form').style.display = 'block';
document.getElementById('back-button').style.display = 'none';
document.querySelector('h1').style.display = 'block';
document.getElementById('content').innerHTML = '';
// Remove query parameter without reloading
window.history.pushState({}, '', window.location.pathname);
}
// Load sheet data
async function loadSheet() {
const urlInput = document.getElementById('sheets-url');
const errorMsg = document.getElementById('error-message');
const url = urlInput.value.trim();
try {
const sheetId = getSheetId(url);
if (!sheetId) {
throw new Error('Invalid Google Sheets URL');
}
// Try fetching directly from Google Sheets as TSV
const sheetUrl = `https://docs.google.com/spreadsheets/d/${sheetId}/export?format=tsv`;
console.log('Attempting direct fetch');
let response, tsvData;
response = await fetch(sheetUrl);
if (!response.ok) throw new Error('Direct fetch failed');
tsvData = await response.text();
console.log('Raw TSV:', tsvData);
// Parse TSV data
const rows = tsvData.split(/\r?\n/).map(line => {
// Split by tabs and trim each field
return line.split('\t').map(field => field.trim());
});
// Remove header row and empty rows, transform to our format
const jsonData = rows.slice(1)
.map(row => ({
type: row[0] || '',
english: row[1] || '',
hebrew: row[2] || ''
}))
.map(row => {
// Check for empty rows that should trigger paragraph breaks
if (!row.type && !row.english && !row.hebrew) {
return { type: 'break', english: '', hebrew: '' };
}
return row;
});
createInterlinear(jsonData);
errorMsg.textContent = '';
// Hide input form and show back button
document.getElementById('input-form').style.display = 'none';
document.getElementById('back-button').style.display = 'block';
document.querySelector('h1').style.display = 'none';
// Update URL with just the sheet ID
const newUrl = new URL(window.location);
newUrl.searchParams.set('id', sheetId);
window.history.pushState({}, '', newUrl);
} catch (error) {
errorMsg.innerHTML = `Error: ${error.message}<br><br>
Tips:<br>
1. Make sure your sheet is shared (File > Share > Anyone with the link)<br>
2. Use the regular Google Sheets URL<br>
3. Check that the sheet contains data with headers: type, english, hebrew<br><br>
<small>Check browser console (F12) for additional debug information</small>`;
console.error('Error:', error);
}
}
// Check for URL parameter on load
window.addEventListener('load', () => {
const params = new URLSearchParams(window.location.search);
const idParam = params.get('id');
if (idParam) {
const urlInput = document.getElementById('sheets-url');
urlInput.value = `https://docs.google.com/spreadsheets/d/${idParam}`;
loadSheet();
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment