Files

681 lines
26 KiB
JavaScript
Raw Permalink Normal View History

2026-09-24 13:05:20 -04:00
// RapidAPI Complete Study Bible Integration Module
// This module provides functions to query the Complete Study Bible API via RapidAPI
// for enhanced word information to display in popup cards
/**
* Extract Strong's numbers from search results
*/
function extractStrongsFromSearchResult(searchResults) {
const strongsSet = new Set();
if (!Array.isArray(searchResults)) return [];
for (const result of searchResults) {
// Look for Strong's fields in various formats
if (result.strongs) {
const strongs = String(result.strongs).match(/([GH]\d+)/gi);
if (strongs) strongs.forEach(s => strongsSet.add(s.toUpperCase()));
}
if (result.strongs_greek) {
const strongs = String(result.strongs_greek).match(/(G\d+)/gi);
if (strongs) strongs.forEach(s => strongsSet.add(s.toUpperCase()));
}
if (result.strongs_hebrew) {
const strongs = String(result.strongs_hebrew).match(/(H\d+)/gi);
if (strongs) strongs.forEach(s => strongsSet.add(s.toUpperCase()));
}
// Only scan fields whose key indicates Strong's data. Scanning ALL string fields produced
// false positives (e.g. 'G20' from verse text, or 'H2' harvested from 'H2O') that were then
// sent to the strongs-detail endpoint and surfaced as unrelated lexical entries.
for (const [key, value] of Object.entries(result)) {
if (typeof value === 'string' && /strong/i.test(key)) {
const matches = value.match(/([GH]\d+)/gi);
if (matches) matches.forEach(m => strongsSet.add(m.toUpperCase()));
}
}
}
return Array.from(strongsSet);
}
/**
* Query RapidAPI Complete Study Bible for word information
* @param {string} word - The word to look up
* @param {string} reference - Optional Bible reference (e.g., "John 3:16")
* @returns {Promise<Object>} Enhanced word information from RapidAPI
*/
export async function queryRapidAPIWord(word, reference = null) {
try {
// Get API key from localStorage (user needs to set this)
const apiKey = typeof localStorage !== 'undefined'
? localStorage.getItem('rapidapi.key')
: null;
if (!apiKey) {
console.warn('[RapidAPI] No API key found. Set localStorage.setItem("rapidapi.key", "your-key")');
return {
success: false,
error: 'API key not configured',
word: word,
reference: reference
};
}
// Get API host from localStorage (defaults to the Complete Study Bible API)
const apiHost = typeof localStorage !== 'undefined'
? localStorage.getItem('rapidapi.host') || 'complete-study-bible.p.rapidapi.com'
: 'complete-study-bible.p.rapidapi.com';
console.log('[RapidAPI] Querying word:', word, 'reference:', reference);
console.log('[RapidAPI] API Host:', apiHost);
console.log('[RapidAPI] API Key:', apiKey ? apiKey.substring(0, 10) + '...' : 'NOT SET');
// Try endpoints based on what we found in the API
// Priority: word search -> get Strong's -> get Strong's details
const normalizedWord = word.toLowerCase().replace(/\s+/g, ''); // Remove spaces for search-exact
const endpoints = [
// Word search endpoints (found in API)
`/search/${encodeURIComponent(word)}/`, // Search for word
`/search-exact/${encodeURIComponent(normalizedWord)}/`, // Exact word search
`/search/${encodeURIComponent(normalizedWord)}/`, // Search without spaces
// Fallback patterns
`/word/${encodeURIComponent(word.toLowerCase())}`,
`/words/${encodeURIComponent(word.toLowerCase())}`,
`/search?word=${encodeURIComponent(word.toLowerCase())}`
];
for (const endpoint of endpoints) {
try {
const url = `https://${apiHost}${endpoint}`;
console.log('[RapidAPI] Trying endpoint:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': apiHost,
'Content-Type': 'application/json'
}
});
console.log('[RapidAPI] Response status:', response.status, 'for endpoint:', endpoint);
if (response.ok) {
const data = await response.json();
console.log('[RapidAPI] ✅ SUCCESS! Response data:', data);
// If this is a search result that might contain Strong's numbers, try to get details
let finalData = data;
if (Array.isArray(data) && data.length > 0) {
// Search result - try to extract Strong's numbers and get details
const strongsNumbers = extractStrongsFromSearchResult(data);
if (strongsNumbers.length > 0) {
console.log('[RapidAPI] Found Strong\'s numbers:', strongsNumbers);
// Get Strong's details
const detailsEndpoint = `/strongs-detail/${strongsNumbers.join(',')}/`;
try {
const detailsUrl = `https://${apiHost}${detailsEndpoint}`;
const detailsResponse = await fetch(detailsUrl, {
method: 'GET',
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': apiHost,
'Content-Type': 'application/json'
}
});
if (detailsResponse.ok) {
const detailsData = await detailsResponse.json();
console.log('[RapidAPI] ✅ Got Strong\'s details:', detailsData);
finalData = { searchResults: data, strongsDetails: detailsData };
}
} catch (err) {
console.log('[RapidAPI] Could not fetch Strong\'s details:', err);
}
}
}
return {
success: true,
data: finalData,
word: word,
reference: reference,
endpoint: endpoint
};
} else {
const errorText = await response.text();
console.log('[RapidAPI] ❌ Failed:', response.status, errorText.substring(0, 200));
// If it's a 401/403, stop trying - it's an auth issue
if (response.status === 401 || response.status === 403) {
return {
success: false,
error: `Authentication failed (${response.status}). Check your API key.`,
word: word,
reference: reference
};
}
// Continue to next endpoint
}
} catch (error) {
console.log('[RapidAPI] Error trying endpoint:', endpoint, error.message);
// Continue to next endpoint
}
}
// If we get here, all endpoints failed
return {
success: false,
error: 'No working endpoint found. Check RapidAPI playground for correct endpoint structure.',
word: word,
reference: reference
};
} catch (error) {
console.warn('[RapidAPI] Failed to query word:', word, error);
return {
success: false,
error: error.message || 'Network error',
word: word,
reference: reference
};
}
}
// Removed - now trying all endpoints in main function
/**
* Get current Bible reference from active tab
* @returns {string|null} Current reference or null
*/
function getCurrentReference() {
try {
const activeTab = window.bibleApp?.tabs?.find(t => t.id === window.bibleApp?.activeTabId);
if (!activeTab) return null;
const book = activeTab.book || '';
const chapter = activeTab.chapter || '';
const verse = activeTab.verse || '';
if (book && chapter) {
return verse ? `${book} ${chapter}:${verse}` : `${book} ${chapter}`;
}
return null;
} catch (_) {
return null;
}
}
/**
* Parse RapidAPI response into structured word info format
* This function adapts the API response to match your popup structure
* Adjust based on actual API response format
*/
export function parseRapidAPIResponse(apiResponse, word) {
try {
console.log('[RapidAPI] Parsing response:', apiResponse);
const data = apiResponse.data || apiResponse;
console.log('[RapidAPI] Data structure:', Array.isArray(data) ? `Array[${data.length}]` : Object.keys(data || {}));
// Extract information based on common API response structures
// Adjust these mappings based on actual API response format
const result = {
greek: null,
hebrew: null,
strongs: null,
definition: null,
transliteration: null,
commentaries: [],
crossReferences: [],
usage: null,
morphology: null
};
// Handle Strong's details response (most useful!)
if (data.strongsDetails || (Array.isArray(data) && data.length > 0 && (data[0].strongs || data[0].number || data[0].greek || data[0].hebrew))) {
const strongsData = data.strongsDetails || data;
const strongsArray = Array.isArray(strongsData) ? strongsData : [strongsData];
for (const item of strongsArray) {
// Extract Strong's number - try many field names
if (!result.strongs) {
const strongs = item.strongs || item.number || item.strongs_number || item.strong || item.id;
if (strongs) {
result.strongs = String(strongs).toUpperCase();
}
}
// Extract definition - try many field names
if (!result.definition) {
const def = item.definition || item.meaning || item.gloss || item.translation ||
item.english || item.text || item.description;
if (def) {
result.definition = String(def);
}
}
// Extract Greek - try many field names and patterns
if (!result.greek || !result.greek.text || result.greek.text === '—') {
const greekText = item.greek || item.greek_text || item.original_greek || item.greek_word ||
item.greek_lemma || item.lemma || item.word_greek;
const greekTranslit = item.greek_transliteration || item.transliteration || item.translit ||
item.greek_translit || item.greek_trans;
if (greekText) {
result.greek = {
text: String(greekText),
transliteration: greekTranslit ? String(greekTranslit) : '',
meaning: result.definition || String(item.definition || item.meaning || '')
};
}
}
// Extract Hebrew - try many field names
if (!result.hebrew || !result.hebrew.text || result.hebrew.text === '—') {
const hebrewText = item.hebrew || item.hebrew_text || item.original_hebrew || item.hebrew_word ||
item.hebrew_lemma || item.word_hebrew;
const hebrewTranslit = item.hebrew_transliteration || item.transliteration || item.translit ||
item.hebrew_translit || item.hebrew_trans;
if (hebrewText) {
result.hebrew = {
text: String(hebrewText),
transliteration: hebrewTranslit ? String(hebrewTranslit) : '',
meaning: result.definition || String(item.definition || item.meaning || '')
};
}
}
// Extract transliteration
if (!result.transliteration) {
const translit = item.transliteration || item.translit || item.trans;
if (translit) {
result.transliteration = String(translit);
}
}
// Extract morphology
if (!result.morphology) {
const morph = item.morphology || item.grammar || item.morph || item.grammatical;
if (morph) {
result.morphology = String(morph);
}
}
}
// Also handle search results if present - calculate OT/NT usage
if (data.searchResults && Array.isArray(data.searchResults)) {
const OT_BOOKS = new Set(['Genesis','Exodus','Leviticus','Numbers','Deuteronomy','Joshua','Judges','Ruth','1 Samuel','2 Samuel','1 Kings','2 Kings','1 Chronicles','2 Chronicles','Ezra','Nehemiah','Esther','Job','Psalms','Proverbs','Ecclesiastes','Song of Solomon','Isaiah','Jeremiah','Lamentations','Ezekiel','Daniel','Hosea','Joel','Amos','Obadiah','Jonah','Micah','Nahum','Habakkuk','Zephaniah','Haggai','Zechariah','Malachi']);
const NT_BOOKS = new Set(['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']);
let otCount = 0;
let ntCount = 0;
result.crossReferences = data.searchResults.map(verse => {
const book = verse.book__name || verse.book__abbrev || '';
if (OT_BOOKS.has(book)) otCount++;
if (NT_BOOKS.has(book)) ntCount++;
const chapter = verse.chapter || '';
const verseNum = verse.verse || '';
return `${book} ${chapter}:${verseNum}`;
});
result.usage = `Found in ${data.searchResults.length} verse(s)`;
result.usageOT = otCount;
result.usageNT = ntCount;
result.usageTotal = data.searchResults.length;
}
console.log('[RapidAPI] Parsed Strong\'s details:', result);
return result;
}
// Handle array responses (like verse search results)
if (Array.isArray(data) && data.length > 0) {
// This looks like a verse search result - extract cross-references and calculate OT/NT usage
const OT_BOOKS = new Set(['Genesis','Exodus','Leviticus','Numbers','Deuteronomy','Joshua','Judges','Ruth','1 Samuel','2 Samuel','1 Kings','2 Kings','1 Chronicles','2 Chronicles','Ezra','Nehemiah','Esther','Job','Psalms','Proverbs','Ecclesiastes','Song of Solomon','Isaiah','Jeremiah','Lamentations','Ezekiel','Daniel','Hosea','Joel','Amos','Obadiah','Jonah','Micah','Nahum','Habakkuk','Zephaniah','Haggai','Zechariah','Malachi']);
const NT_BOOKS = new Set(['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']);
let otCount = 0;
let ntCount = 0;
result.crossReferences = data.map(verse => {
const book = verse.book__name || verse.book__abbrev || '';
if (OT_BOOKS.has(book)) otCount++;
if (NT_BOOKS.has(book)) ntCount++;
const chapter = verse.chapter || '';
const verseNum = verse.verse || '';
return `${book} ${chapter}:${verseNum}`;
});
result.usage = `Found in ${data.length} verse(s)`;
result.usageOT = otCount;
result.usageNT = ntCount;
result.usageTotal = data.length;
// Try to extract Strong's from the endpoint if available
if (apiResponse.endpoint && apiResponse.endpoint.includes('strongs/')) {
const match = apiResponse.endpoint.match(/strongs\/([GH]\d+)/i);
if (match) {
result.strongs = match[1].toUpperCase();
}
}
console.log('[RapidAPI] Parsed verse array:', result);
return result;
}
// Try to extract ANY text that might be useful
// Check for nested structures
const checkNested = (obj, paths) => {
for (const path of paths) {
const keys = path.split('.');
let value = obj;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
value = null;
break;
}
}
if (value) return value;
}
return null;
};
// Extract definition - try many possible paths
const definition = checkNested(data, [
'definition', 'meaning', 'gloss', 'text', 'description',
'data.definition', 'result.definition', 'word.definition',
'definitions[0]', 'definitions.0'
]);
if (definition) result.definition = String(definition);
// Extract Strong's numbers
const strongs = checkNested(data, [
'strongs', 'strongsNumber', 'strong', 'strongNumber',
'data.strongs', 'result.strongs'
]);
if (strongs) result.strongs = String(strongs);
// Extract Greek
const greekText = checkNested(data, [
'greek', 'greekText', 'originalGreek', 'greek.word', 'greek.text'
]);
if (greekText) {
result.greek = {
text: String(greekText),
transliteration: checkNested(data, ['greekTransliteration', 'greek.transliteration']) || '',
meaning: result.definition || ''
};
}
// Extract Hebrew
const hebrewText = checkNested(data, [
'hebrew', 'hebrewText', 'originalHebrew', 'hebrew.word', 'hebrew.text'
]);
if (hebrewText) {
result.hebrew = {
text: String(hebrewText),
transliteration: checkNested(data, ['hebrewTransliteration', 'hebrew.transliteration']) || '',
meaning: result.definition || ''
};
}
// Extract transliteration
const transliteration = checkNested(data, [
'transliteration', 'translit', 'data.transliteration'
]);
if (transliteration) result.transliteration = String(transliteration);
// Extract commentaries
const commentaries = checkNested(data, [
'commentaries', 'commentary', 'comments', 'data.commentaries'
]);
if (Array.isArray(commentaries)) {
result.commentaries = commentaries.map(c => String(c));
} else if (commentaries) {
result.commentaries = [String(commentaries)];
}
// Extract cross-references
const crossRefs = checkNested(data, [
'crossReferences', 'references', 'crossRefs', 'xrefs'
]);
if (Array.isArray(crossRefs)) {
result.crossReferences = crossRefs.map(r => String(r));
} else if (crossRefs) {
result.crossReferences = [String(crossRefs)];
}
// Extract usage
const usage = checkNested(data, [
'usage', 'usageCount', 'frequency', 'count'
]);
if (usage) {
result.usage = typeof usage === 'number' ? `Used ${usage} times` : String(usage);
}
// Extract morphology
const morphology = checkNested(data, [
'morphology', 'grammar', 'morph', 'grammatical'
]);
if (morphology) result.morphology = String(morphology);
// If we got ANY data, return it
const hasData = result.definition || result.strongs || result.greek || result.hebrew ||
result.commentaries.length > 0 || result.crossReferences.length > 0;
if (hasData) {
console.log('[RapidAPI] ✅ Parsed data:', result);
return result;
} else {
console.log('[RapidAPI] ⚠️ No recognizable data found in response');
// Return raw data so user can see what we got
return {
definition: `Raw API Response: ${JSON.stringify(data).substring(0, 200)}...`,
rawData: data
};
}
} catch (error) {
console.warn('[RapidAPI] Error parsing response:', error);
return null;
}
}
/**
* Enhance word info object with RapidAPI Complete Study Bible data
* @param {Object} wordInfo - Base word info object
* @param {string} word - The word being looked up
* @param {string} reference - Optional Bible reference
* @returns {Promise<Object>} Enhanced word info with RapidAPI data
*/
export async function enhanceWordInfoWithRapidAPI(wordInfo, word, reference = null) {
try {
console.log('[RapidAPI] Enhancing word info for:', word, 'reference:', reference);
const rapidAPIData = await queryRapidAPIWord(word, reference);
console.log('[RapidAPI] Received data:', rapidAPIData);
// Merge RapidAPI data into word info
const enhanced = { ...wordInfo };
if (rapidAPIData.success && rapidAPIData.data) {
const parsed = parseRapidAPIResponse(rapidAPIData, word);
if (parsed) {
// CRITICAL: Only enhance if we have Greek definition (user requirement)
const hasGreekDefinition = parsed.greek && parsed.greek.text && parsed.greek.text !== '—' && parsed.greek.text.trim().length > 0;
if (hasGreekDefinition) {
// Enhance Greek data - this is required!
enhanced.greek = {
text: parsed.greek.text,
transliteration: parsed.greek.transliteration || '',
meaning: parsed.greek.meaning || parsed.definition || ''
};
// Mark as having valid data
enhanced.hasValidData = true;
}
// Enhance Hebrew data if available (optional)
if (parsed.hebrew && parsed.hebrew.text && parsed.hebrew.text !== '—') {
enhanced.hebrew = {
text: parsed.hebrew.text,
transliteration: parsed.hebrew.transliteration || '',
meaning: parsed.hebrew.meaning || parsed.definition || ''
};
}
// Update usage counts from RapidAPI search results
if (parsed.usageOT !== undefined) {
enhanced.frequencyOT = parsed.usageOT;
}
if (parsed.usageNT !== undefined) {
enhanced.frequencyNT = parsed.usageNT;
}
if (parsed.usageTotal !== undefined) {
enhanced.frequency = parsed.usageTotal;
}
// Add RapidAPI section with additional data
enhanced.rapidAPI = {
available: true,
hasGreekDefinition: hasGreekDefinition,
strongs: parsed.strongs || null,
definition: parsed.definition || null,
transliteration: parsed.transliteration || null,
commentaries: parsed.commentaries || [],
crossReferences: parsed.crossReferences || [],
usage: parsed.usage || null,
usageOT: parsed.usageOT || null,
usageNT: parsed.usageNT || null,
usageTotal: parsed.usageTotal || null,
morphology: parsed.morphology || null
};
} else {
enhanced.rapidAPI = {
available: true,
error: 'Could not parse API response',
rawData: rapidAPIData.data
};
}
} else {
// API call failed or returned error
enhanced.rapidAPI = {
available: false,
error: rapidAPIData.error || 'Unknown error',
note: 'RapidAPI Complete Study Bible integration is configured but returned no data'
};
}
return enhanced;
} catch (error) {
console.warn('[RapidAPI] Error enhancing word info:', error);
return {
...wordInfo,
rapidAPI: {
available: false,
error: error.message || 'Unknown error'
}
};
}
}
/**
* Check if RapidAPI is available and configured
* @returns {Promise<boolean>} True if API is available
*/
export async function checkRapidAPIAvailability() {
try {
const apiKey = typeof localStorage !== 'undefined'
? localStorage.getItem('rapidapi.key')
: null;
if (!apiKey) {
console.log('[RapidAPI] Not configured - no API key found');
return false;
}
// Try a simple test query
const testResponse = await queryRapidAPIWord('test');
return testResponse.success === true;
} catch (error) {
console.warn('[RapidAPI] Availability check failed:', error);
return false;
}
}
// Make functions globally available for testing
if (typeof window !== 'undefined') {
window.queryRapidAPIWord = queryRapidAPIWord;
window.enhanceWordInfoWithRapidAPI = enhanceWordInfoWithRapidAPI;
window.checkRapidAPIAvailability = checkRapidAPIAvailability;
// Simple test function
window.testRapidAPIDirect = async function(word = 'love') {
console.log('🧪 Testing RapidAPI directly for word:', word);
console.log('🧪 API Key:', localStorage.getItem('rapidapi.key') ? 'SET' : 'NOT SET');
console.log('🧪 API Host:', localStorage.getItem('rapidapi.host') || 'complete-study-bible.p.rapidapi.com');
const result = await queryRapidAPIWord(word);
console.log('🧪 Result:', result);
if (result.success) {
console.log('✅ API call succeeded!');
console.log('📦 Response data:', result.data);
} else {
console.log('❌ API call failed:', result.error);
console.log('💡 TIP: Check the RapidAPI playground to find the correct endpoint structure');
console.log('💡 TIP: Make sure you\'re subscribed to the API on RapidAPI');
}
return result;
};
// Test a custom endpoint (once you find it in the playground)
window.testRapidAPICustomEndpoint = async function(endpoint, word = 'love') {
const apiKey = localStorage.getItem('rapidapi.key');
const apiHost = localStorage.getItem('rapidapi.host') || 'complete-study-bible.p.rapidapi.com';
if (!apiKey) {
console.error('❌ API key not set!');
return;
}
// Replace {word} or similar placeholders
const url = `https://${apiHost}${endpoint.replace('{word}', word).replace('{WORD}', word)}`;
console.log('🧪 Testing custom endpoint:', url);
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': apiHost,
'Content-Type': 'application/json'
}
});
console.log('📊 Response status:', response.status);
if (response.ok) {
const data = await response.json();
console.log('✅ SUCCESS! Response:', data);
return { success: true, data };
} else {
const errorText = await response.text();
console.log('❌ Failed:', response.status, errorText.substring(0, 200));
return { success: false, error: `HTTP ${response.status}` };
}
} catch (error) {
console.error('❌ Error:', error);
return { success: false, error: error.message };
}
};
}