1
1
// PyHC Documentation Hub - Unified Search
2
+ // This script uses public CORS proxy services to make cross-origin requests to the Read the Docs API.
3
+ // Multiple proxy services are tried in sequence for redundancy.
2
4
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
3
5
// Add scroll event handler to toggle shadow on tabs when scrolling
4
6
function initScrollShadow ( ) {
@@ -28,8 +30,29 @@ document.addEventListener('DOMContentLoaded', function() {
28
30
const rtdSearchForm = document . getElementById ( 'rtd-search-form' ) ;
29
31
const rtdSearchInput = rtdSearchForm ? rtdSearchForm . querySelector ( 'input[type="text"]' ) : null ;
30
32
31
- // Base API URL
32
- const apiBaseUrl = 'https://readthedocs.org/api/v3/search/' ;
33
+ // Base API URL with public CORS proxy services
34
+ const rtdApiUrl = 'https://readthedocs.org/api/v3/search/' ;
35
+
36
+ // List of CORS proxies to try (in order of preference)
37
+ const corsProxies = [
38
+ 'https://corsproxy.io/?' ,
39
+ 'https://cors-anywhere.herokuapp.com/' ,
40
+ 'https://api.allorigins.win/raw?url='
41
+ ] ;
42
+
43
+ // Start with the first proxy
44
+ let currentProxyIndex = 0 ;
45
+ const corsProxyUrl = corsProxies [ currentProxyIndex ] ;
46
+ let apiBaseUrl = corsProxyUrl + encodeURIComponent ( rtdApiUrl ) ;
47
+
48
+ // Function to switch to the next proxy if one fails
49
+ function switchToNextProxy ( ) {
50
+ currentProxyIndex = ( currentProxyIndex + 1 ) % corsProxies . length ;
51
+ const nextProxy = corsProxies [ currentProxyIndex ] ;
52
+ apiBaseUrl = nextProxy + encodeURIComponent ( rtdApiUrl ) ;
53
+ console . log ( `Switched to CORS proxy: ${ nextProxy } ` ) ;
54
+ return apiBaseUrl ;
55
+ }
33
56
34
57
// Modal elements (will be defined after createSearchModal)
35
58
const searchModal = document . getElementById ( 'pyhc-search-modal' ) ;
@@ -446,8 +469,11 @@ document.addEventListener('DOMContentLoaded', function() {
446
469
// Build the full search query with space between project list and search term
447
470
const fullQuery = `${ projectsQuery } ${ query } ` ;
448
471
449
- // Build the URL with the correctly encoded query parameter
450
- const queryUrl = `${ apiBaseUrl } ?q=${ encodeURIComponent ( fullQuery ) } ` ;
472
+ // Build the query parameter
473
+ const queryParam = `q=${ encodeURIComponent ( fullQuery ) } ` ;
474
+
475
+ // When using corsproxy.io, we need to add the query parameter to the encoded URL
476
+ const queryUrl = `${ apiBaseUrl } ${ encodeURIComponent ( '?' + queryParam ) } ` ;
451
477
452
478
// Fetch results
453
479
fetch ( queryUrl )
@@ -463,9 +489,43 @@ document.addEventListener('DOMContentLoaded', function() {
463
489
displayResults ( data , query ) ;
464
490
} )
465
491
. catch ( error => {
466
- // Reset search icon to magnifying glass
467
- resetSearchIcon ( ) ;
468
- showMessage ( `An error occurred while searching: ${ error . message } . Please try again later.` ) ;
492
+ console . error ( "Search error:" , error ) ;
493
+
494
+ // Check if we've tried all proxies
495
+ if ( corsProxies . length > 1 ) {
496
+ // Try with next proxy
497
+ switchToNextProxy ( ) ;
498
+
499
+ // Update the query URL with the new proxy
500
+ const newQueryUrl = `${ apiBaseUrl } ${ encodeURIComponent ( '?' + queryParam ) } ` ;
501
+
502
+ // Show a trying alternative proxy message
503
+ showMessage ( `Trying alternative proxy service... Please wait.` ) ;
504
+
505
+ // Retry the search with the new proxy
506
+ setTimeout ( ( ) => {
507
+ fetch ( newQueryUrl )
508
+ . then ( response => {
509
+ if ( ! response . ok ) {
510
+ throw new Error ( `Network response was not ok: ${ response . status } ${ response . statusText } ` ) ;
511
+ }
512
+ return response . json ( ) ;
513
+ } )
514
+ . then ( data => {
515
+ resetSearchIcon ( ) ;
516
+ displayResults ( data , query ) ;
517
+ } )
518
+ . catch ( retryError => {
519
+ // If all proxies fail, show error message
520
+ resetSearchIcon ( ) ;
521
+ showMessage ( `Search failed. The search service may be temporarily unavailable. Please try again later.` ) ;
522
+ } ) ;
523
+ } , 1000 ) ;
524
+ } else {
525
+ // Reset search icon to magnifying glass
526
+ resetSearchIcon ( ) ;
527
+ showMessage ( `An error occurred while searching: ${ error . message } . Please try again later.` ) ;
528
+ }
469
529
} ) ;
470
530
}
471
531
@@ -869,7 +929,10 @@ document.addEventListener('DOMContentLoaded', function() {
869
929
loadMoreBtn . disabled = true ;
870
930
}
871
931
872
- fetch ( nextUrl )
932
+ // We need to handle the next URL through the CORS proxy
933
+ const proxiedNextUrl = corsProxyUrl + encodeURIComponent ( nextUrl ) ;
934
+
935
+ fetch ( proxiedNextUrl )
873
936
. then ( response => {
874
937
if ( ! response . ok ) {
875
938
throw new Error ( 'Network response was not ok' ) ;
@@ -981,9 +1044,142 @@ document.addEventListener('DOMContentLoaded', function() {
981
1044
} )
982
1045
. catch ( error => {
983
1046
console . error ( 'Error loading more results:' , error ) ;
984
- if ( loadMoreBtn ) {
985
- loadMoreBtn . textContent = 'Error loading more results. Click to try again.' ;
986
- loadMoreBtn . disabled = false ;
1047
+
1048
+ // Try with the next proxy if available
1049
+ if ( corsProxies . length > 1 ) {
1050
+ switchToNextProxy ( ) ;
1051
+ const retryProxiedUrl = corsProxyUrl + encodeURIComponent ( nextUrl ) ;
1052
+
1053
+ if ( loadMoreBtn ) {
1054
+ loadMoreBtn . textContent = 'Trying alternative proxy service...' ;
1055
+ }
1056
+
1057
+ // Retry with new proxy
1058
+ setTimeout ( ( ) => {
1059
+ fetch ( retryProxiedUrl )
1060
+ . then ( response => {
1061
+ if ( ! response . ok ) {
1062
+ throw new Error ( 'Network response was not ok' ) ;
1063
+ }
1064
+ return response . json ( ) ;
1065
+ } )
1066
+ . then ( data => {
1067
+ // Remove the load more button
1068
+ if ( loadMoreBtn ) {
1069
+ loadMoreBtn . remove ( ) ;
1070
+ }
1071
+
1072
+ // Continue with processing the data as before
1073
+ // Group new results by project
1074
+ const resultsByProject = { } ;
1075
+
1076
+ data . results . forEach ( result => {
1077
+ const projectSlug = result . project . slug ;
1078
+ if ( ! resultsByProject [ projectSlug ] ) {
1079
+ resultsByProject [ projectSlug ] = [ ] ;
1080
+ }
1081
+ resultsByProject [ projectSlug ] . push ( result ) ;
1082
+ } ) ;
1083
+
1084
+ // Get existing project tabs container
1085
+ const tabsContainer = resultsContainer . querySelector ( '.project-tabs' ) ;
1086
+
1087
+ // Process each project's results
1088
+ for ( const [ project , results ] of Object . entries ( resultsByProject ) ) {
1089
+ // Check if there's already a container for this project
1090
+ let projectContainer = resultsContainer . querySelector ( `.project-results[data-project="${ project } "]` ) ;
1091
+
1092
+ // If there's no tab for this project yet, create it
1093
+ if ( ! projectContainer ) {
1094
+ // Create a new tab for this project
1095
+ const tab = document . createElement ( 'div' ) ;
1096
+ tab . className = 'project-tab' ;
1097
+ tab . dataset . project = project ;
1098
+ tab . innerHTML = `
1099
+ <div class="project-tab-icon">
1100
+ <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
1101
+ <path fill="currentColor" d="M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z"></path>
1102
+ </svg>
1103
+ </div>
1104
+ ${ project } (${ results . length } )
1105
+ ` ;
1106
+
1107
+ // Add tab click handler
1108
+ tab . addEventListener ( 'click' , function ( ) {
1109
+ // Remove active class from all tabs and containers
1110
+ document . querySelectorAll ( '.project-tab' ) . forEach ( t => t . classList . remove ( 'active' ) ) ;
1111
+ document . querySelectorAll ( '.project-results' ) . forEach ( c => c . classList . remove ( 'active' ) ) ;
1112
+
1113
+ // Add active class to this tab and its container
1114
+ tab . classList . add ( 'active' ) ;
1115
+ const container = resultsContainer . querySelector ( `.project-results[data-project="${ project } "]` ) ;
1116
+ if ( container ) container . classList . add ( 'active' ) ;
1117
+ } ) ;
1118
+
1119
+ tabsContainer . appendChild ( tab ) ;
1120
+
1121
+ // Create a new container for this project
1122
+ projectContainer = document . createElement ( 'div' ) ;
1123
+ projectContainer . className = 'project-results' ;
1124
+ projectContainer . dataset . project = project ;
1125
+
1126
+ // Find the load more button to insert before it (if it exists)
1127
+ const loadMoreBtn = resultsContainer . querySelector ( '.pyhc-load-more' ) ;
1128
+ if ( loadMoreBtn ) {
1129
+ resultsContainer . insertBefore ( projectContainer , loadMoreBtn ) ;
1130
+ } else {
1131
+ resultsContainer . appendChild ( projectContainer ) ;
1132
+ }
1133
+ } else {
1134
+ // If this project already exists, update the count in the tab
1135
+ const projectTab = tabsContainer . querySelector ( `.project-tab[data-project="${ project } "]` ) ;
1136
+ if ( projectTab ) {
1137
+ // Get current count from tab
1138
+ const currentCount = parseInt ( projectTab . innerText . match ( / \( ( \d + ) \) / ) [ 1 ] ) ;
1139
+ // Update tab text with new count
1140
+ const newCount = currentCount + results . length ;
1141
+ projectTab . innerHTML = projectTab . innerHTML . replace ( / \( \d + \) / , `(${ newCount } )` ) ;
1142
+ }
1143
+ }
1144
+
1145
+ // Add each result for this project to its container
1146
+ results . forEach ( result => {
1147
+ const resultItem = createResultItem ( result ) ;
1148
+ projectContainer . appendChild ( resultItem ) ;
1149
+ } ) ;
1150
+ }
1151
+
1152
+ // Add pagination if needed
1153
+ if ( data . next ) {
1154
+ // Calculate remaining pages
1155
+ const currentResultsCount = Array . from ( document . querySelectorAll ( '.project-results .hit-block' ) ) . length ;
1156
+ const totalResultsCount = data . count ;
1157
+ const remainingResults = totalResultsCount - currentResultsCount ;
1158
+ const remainingPages = Math . ceil ( remainingResults / 50 ) ;
1159
+
1160
+ const newLoadMoreBtn = document . createElement ( 'button' ) ;
1161
+ newLoadMoreBtn . className = 'pyhc-load-more' ;
1162
+ newLoadMoreBtn . textContent = `Load more results (${ remainingPages } ${ remainingPages === 1 ? 'page' : 'pages' } remaining)` ;
1163
+ newLoadMoreBtn . addEventListener ( 'click' , function ( ) {
1164
+ loadNextPage ( data . next ) ;
1165
+ } ) ;
1166
+ resultsContainer . appendChild ( newLoadMoreBtn ) ;
1167
+ }
1168
+ } )
1169
+ . catch ( retryError => {
1170
+ // If retry fails, show error
1171
+ if ( loadMoreBtn ) {
1172
+ loadMoreBtn . textContent = 'Error loading more results. Click to try again.' ;
1173
+ loadMoreBtn . disabled = false ;
1174
+ }
1175
+ } ) ;
1176
+ } , 1000 ) ;
1177
+ } else {
1178
+ // If no more proxies to try
1179
+ if ( loadMoreBtn ) {
1180
+ loadMoreBtn . textContent = 'Error loading more results. Click to try again.' ;
1181
+ loadMoreBtn . disabled = false ;
1182
+ }
987
1183
}
988
1184
} ) ;
989
1185
}
0 commit comments