opencode-replay 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Prism.js Theme - GitHub inspired
3
+ * Optimized for OpenCode Replay with both light and dark mode support
4
+ */
5
+
6
+ code[class*="language-"],
7
+ pre[class*="language-"] {
8
+ color: #24292e;
9
+ background: none;
10
+ font-family: var(--font-mono);
11
+ font-size: var(--font-size-sm);
12
+ text-align: left;
13
+ white-space: pre;
14
+ word-spacing: normal;
15
+ word-break: normal;
16
+ word-wrap: normal;
17
+ line-height: 1.5;
18
+ tab-size: 2;
19
+ hyphens: none;
20
+ }
21
+
22
+ /* Code blocks */
23
+ pre[class*="language-"] {
24
+ padding: var(--spacing-md);
25
+ margin: var(--spacing-md) 0;
26
+ overflow: auto;
27
+ border-radius: var(--radius-md);
28
+ background: var(--code-bg);
29
+ border: 1px solid var(--code-border);
30
+ }
31
+
32
+ /* Inline code */
33
+ :not(pre) > code[class*="language-"] {
34
+ padding: 2px 6px;
35
+ border-radius: var(--radius-sm);
36
+ background: var(--code-bg);
37
+ border: 1px solid var(--code-border);
38
+ }
39
+
40
+ /* Token colors - Light mode */
41
+ .token.comment,
42
+ .token.prolog,
43
+ .token.doctype,
44
+ .token.cdata {
45
+ color: #6a737d;
46
+ font-style: italic;
47
+ }
48
+
49
+ .token.punctuation {
50
+ color: #24292e;
51
+ }
52
+
53
+ .token.namespace {
54
+ opacity: 0.7;
55
+ }
56
+
57
+ .token.property,
58
+ .token.tag,
59
+ .token.boolean,
60
+ .token.number,
61
+ .token.constant,
62
+ .token.symbol,
63
+ .token.deleted {
64
+ color: #005cc5;
65
+ }
66
+
67
+ .token.selector,
68
+ .token.attr-name,
69
+ .token.string,
70
+ .token.char,
71
+ .token.builtin,
72
+ .token.inserted {
73
+ color: #22863a;
74
+ }
75
+
76
+ .token.operator,
77
+ .token.entity,
78
+ .token.url,
79
+ .language-css .token.string,
80
+ .style .token.string {
81
+ color: #d73a49;
82
+ }
83
+
84
+ .token.atrule,
85
+ .token.attr-value,
86
+ .token.keyword {
87
+ color: #d73a49;
88
+ }
89
+
90
+ .token.function,
91
+ .token.class-name {
92
+ color: #6f42c1;
93
+ }
94
+
95
+ .token.regex,
96
+ .token.important,
97
+ .token.variable {
98
+ color: #e36209;
99
+ }
100
+
101
+ .token.important,
102
+ .token.bold {
103
+ font-weight: bold;
104
+ }
105
+
106
+ .token.italic {
107
+ font-style: italic;
108
+ }
109
+
110
+ .token.entity {
111
+ cursor: help;
112
+ }
113
+
114
+ /* Line highlighting */
115
+ .line-highlight {
116
+ background: rgba(255, 255, 0, 0.1);
117
+ border-left: 3px solid #f1c40f;
118
+ margin-left: calc(-1 * var(--spacing-md) - 3px);
119
+ padding-left: calc(var(--spacing-md) + 3px);
120
+ }
121
+
122
+ /* Line numbers */
123
+ .line-numbers .line-numbers-rows {
124
+ position: absolute;
125
+ pointer-events: none;
126
+ top: 0;
127
+ font-size: 100%;
128
+ left: -3.8em;
129
+ width: 3em;
130
+ letter-spacing: -1px;
131
+ border-right: 1px solid var(--color-border);
132
+ user-select: none;
133
+ }
134
+
135
+ .line-numbers-rows > span {
136
+ display: block;
137
+ counter-increment: linenumber;
138
+ }
139
+
140
+ .line-numbers-rows > span:before {
141
+ content: counter(linenumber);
142
+ color: var(--color-text-muted);
143
+ display: block;
144
+ padding-right: 0.8em;
145
+ text-align: right;
146
+ }
147
+
148
+ /* ============================================================================
149
+ Dark Mode
150
+ ============================================================================ */
151
+
152
+ :root[data-theme="dark"] code[class*="language-"],
153
+ :root[data-theme="dark"] pre[class*="language-"] {
154
+ color: #e6edf3;
155
+ }
156
+
157
+ :root[data-theme="dark"] .token.comment,
158
+ :root[data-theme="dark"] .token.prolog,
159
+ :root[data-theme="dark"] .token.doctype,
160
+ :root[data-theme="dark"] .token.cdata {
161
+ color: #8b949e;
162
+ }
163
+
164
+ :root[data-theme="dark"] .token.punctuation {
165
+ color: #e6edf3;
166
+ }
167
+
168
+ :root[data-theme="dark"] .token.property,
169
+ :root[data-theme="dark"] .token.tag,
170
+ :root[data-theme="dark"] .token.boolean,
171
+ :root[data-theme="dark"] .token.number,
172
+ :root[data-theme="dark"] .token.constant,
173
+ :root[data-theme="dark"] .token.symbol,
174
+ :root[data-theme="dark"] .token.deleted {
175
+ color: #79c0ff;
176
+ }
177
+
178
+ :root[data-theme="dark"] .token.selector,
179
+ :root[data-theme="dark"] .token.attr-name,
180
+ :root[data-theme="dark"] .token.string,
181
+ :root[data-theme="dark"] .token.char,
182
+ :root[data-theme="dark"] .token.builtin,
183
+ :root[data-theme="dark"] .token.inserted {
184
+ color: #a5d6ff;
185
+ }
186
+
187
+ :root[data-theme="dark"] .token.operator,
188
+ :root[data-theme="dark"] .token.entity,
189
+ :root[data-theme="dark"] .token.url,
190
+ :root[data-theme="dark"] .language-css .token.string,
191
+ :root[data-theme="dark"] .style .token.string {
192
+ color: #ff7b72;
193
+ }
194
+
195
+ :root[data-theme="dark"] .token.atrule,
196
+ :root[data-theme="dark"] .token.attr-value,
197
+ :root[data-theme="dark"] .token.keyword {
198
+ color: #ff7b72;
199
+ }
200
+
201
+ :root[data-theme="dark"] .token.function,
202
+ :root[data-theme="dark"] .token.class-name {
203
+ color: #d2a8ff;
204
+ }
205
+
206
+ :root[data-theme="dark"] .token.regex,
207
+ :root[data-theme="dark"] .token.important,
208
+ :root[data-theme="dark"] .token.variable {
209
+ color: #ffa657;
210
+ }
211
+
212
+ /* Auto dark mode */
213
+ @media (prefers-color-scheme: dark) {
214
+ :root:not([data-theme="light"]) code[class*="language-"],
215
+ :root:not([data-theme="light"]) pre[class*="language-"] {
216
+ color: #e6edf3;
217
+ }
218
+
219
+ :root:not([data-theme="light"]) .token.comment,
220
+ :root:not([data-theme="light"]) .token.prolog,
221
+ :root:not([data-theme="light"]) .token.doctype,
222
+ :root:not([data-theme="light"]) .token.cdata {
223
+ color: #8b949e;
224
+ }
225
+
226
+ :root:not([data-theme="light"]) .token.punctuation {
227
+ color: #e6edf3;
228
+ }
229
+
230
+ :root:not([data-theme="light"]) .token.property,
231
+ :root:not([data-theme="light"]) .token.tag,
232
+ :root:not([data-theme="light"]) .token.boolean,
233
+ :root:not([data-theme="light"]) .token.number,
234
+ :root:not([data-theme="light"]) .token.constant,
235
+ :root:not([data-theme="light"]) .token.symbol,
236
+ :root:not([data-theme="light"]) .token.deleted {
237
+ color: #79c0ff;
238
+ }
239
+
240
+ :root:not([data-theme="light"]) .token.selector,
241
+ :root:not([data-theme="light"]) .token.attr-name,
242
+ :root:not([data-theme="light"]) .token.string,
243
+ :root:not([data-theme="light"]) .token.char,
244
+ :root:not([data-theme="light"]) .token.builtin,
245
+ :root:not([data-theme="light"]) .token.inserted {
246
+ color: #a5d6ff;
247
+ }
248
+
249
+ :root:not([data-theme="light"]) .token.operator,
250
+ :root:not([data-theme="light"]) .token.entity,
251
+ :root:not([data-theme="light"]) .token.url,
252
+ :root:not([data-theme="light"]) .language-css .token.string,
253
+ :root:not([data-theme="light"]) .style .token.string {
254
+ color: #ff7b72;
255
+ }
256
+
257
+ :root:not([data-theme="light"]) .token.atrule,
258
+ :root:not([data-theme="light"]) .token.attr-value,
259
+ :root:not([data-theme="light"]) .token.keyword {
260
+ color: #ff7b72;
261
+ }
262
+
263
+ :root:not([data-theme="light"]) .token.function,
264
+ :root:not([data-theme="light"]) .token.class-name {
265
+ color: #d2a8ff;
266
+ }
267
+
268
+ :root:not([data-theme="light"]) .token.regex,
269
+ :root:not([data-theme="light"]) .token.important,
270
+ :root:not([data-theme="light"]) .token.variable {
271
+ color: #ffa657;
272
+ }
273
+ }
@@ -0,0 +1,445 @@
1
+ /**
2
+ * OpenCode Replay - Client-side Search
3
+ *
4
+ * Features:
5
+ * - Fetches paginated HTML pages on-demand
6
+ * - Searches within .message elements using case-insensitive substring matching
7
+ * - Displays results as they're found (streaming UX)
8
+ * - Highlights matches with <mark> tags
9
+ * - Supports URL fragment for shareable searches (#search=query)
10
+ * - Progressive enhancement - hidden for file:// protocol (CORS)
11
+ */
12
+
13
+ (function() {
14
+ 'use strict';
15
+
16
+ // Configuration
17
+ var BATCH_SIZE = 3; // Pages to fetch in parallel
18
+ var MAX_RESULTS = 100; // Maximum results to display
19
+
20
+ // State
21
+ var modal = null;
22
+ var searchInput = null;
23
+ var searchStatus = null;
24
+ var searchResults = null;
25
+ var isSearching = false;
26
+
27
+ // Detect page context
28
+ var isSessionPage = window.location.pathname.includes('/sessions/');
29
+ var isIndexPage = !isSessionPage;
30
+
31
+ // Get total pages from data attribute (set by template)
32
+ var totalPages = parseInt(document.body.dataset.totalPages || '0', 10);
33
+
34
+ /**
35
+ * Check if we can perform search (CORS limitation for file://)
36
+ */
37
+ function canSearch() {
38
+ return window.location.protocol !== 'file:';
39
+ }
40
+
41
+ /**
42
+ * Escape HTML special characters
43
+ */
44
+ function escapeHtml(str) {
45
+ var div = document.createElement('div');
46
+ div.textContent = str;
47
+ return div.innerHTML;
48
+ }
49
+
50
+ /**
51
+ * Escape regex special characters
52
+ */
53
+ function escapeRegex(str) {
54
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
55
+ }
56
+
57
+ /**
58
+ * Create the search modal HTML
59
+ */
60
+ function createModal() {
61
+ var modalHtml = '<dialog id="search-modal" class="search-modal">' +
62
+ '<div class="search-modal-content">' +
63
+ '<div class="search-modal-header">' +
64
+ '<div class="search-input-wrapper">' +
65
+ '<span class="search-input-icon">&#128269;</span>' +
66
+ '<input type="text" id="search-modal-input" placeholder="Search messages..." autocomplete="off" />' +
67
+ '</div>' +
68
+ '<button type="button" class="search-modal-close" aria-label="Close search">&times;</button>' +
69
+ '</div>' +
70
+ '<div id="search-status" class="search-status"></div>' +
71
+ '<div id="search-results" class="search-results"></div>' +
72
+ '</div>' +
73
+ '</dialog>';
74
+
75
+ var container = document.createElement('div');
76
+ container.innerHTML = modalHtml;
77
+ document.body.appendChild(container.firstChild);
78
+
79
+ modal = document.getElementById('search-modal');
80
+ searchInput = document.getElementById('search-modal-input');
81
+ searchStatus = document.getElementById('search-status');
82
+ searchResults = document.getElementById('search-results');
83
+
84
+ // Event listeners
85
+ var closeBtn = modal.querySelector('.search-modal-close');
86
+ closeBtn.addEventListener('click', closeModal);
87
+
88
+ modal.addEventListener('click', function(e) {
89
+ if (e.target === modal) {
90
+ closeModal();
91
+ }
92
+ });
93
+
94
+ modal.addEventListener('cancel', function(e) {
95
+ e.preventDefault();
96
+ closeModal();
97
+ });
98
+
99
+ var debounceTimer = null;
100
+ searchInput.addEventListener('input', function() {
101
+ clearTimeout(debounceTimer);
102
+ debounceTimer = setTimeout(function() {
103
+ performSearch(searchInput.value.trim());
104
+ }, 200);
105
+ });
106
+
107
+ searchInput.addEventListener('keydown', function(e) {
108
+ if (e.key === 'Escape') {
109
+ closeModal();
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Open the search modal
116
+ */
117
+ function openModal(initialQuery) {
118
+ if (!modal) {
119
+ createModal();
120
+ }
121
+
122
+ modal.showModal();
123
+ searchInput.value = initialQuery || '';
124
+ searchInput.focus();
125
+
126
+ if (initialQuery) {
127
+ performSearch(initialQuery);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Close the search modal
133
+ */
134
+ function closeModal() {
135
+ if (modal && modal.open) {
136
+ modal.close();
137
+ isSearching = false;
138
+ // Clear URL hash
139
+ if (window.location.hash.startsWith('#search=')) {
140
+ history.replaceState(null, '', window.location.pathname + window.location.search);
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Update URL hash with search query
147
+ */
148
+ function updateUrlHash(query) {
149
+ if (query) {
150
+ history.replaceState(null, '', window.location.pathname + window.location.search + '#search=' + encodeURIComponent(query));
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Get the URL for fetching a page
156
+ */
157
+ function getPageFetchUrl(pageFile) {
158
+ // Pages are in the same directory as index.html for sessions
159
+ return pageFile;
160
+ }
161
+
162
+ /**
163
+ * Get the URL for linking to a result
164
+ */
165
+ function getPageLinkUrl(pageFile) {
166
+ return pageFile;
167
+ }
168
+
169
+ /**
170
+ * Highlight search term in text by escaping HTML first, then wrapping matches
171
+ * This prevents XSS by ensuring the query is never directly injected as HTML
172
+ */
173
+ function highlightMatches(text, searchTerm) {
174
+ if (!text || !searchTerm) return escapeHtml(text || '');
175
+
176
+ // First escape the entire text to prevent XSS
177
+ var escaped = escapeHtml(text);
178
+ // Also escape the search term for HTML (in case it contains < > etc)
179
+ var escapedTerm = escapeHtml(searchTerm);
180
+
181
+ // Now highlight matches in the escaped text
182
+ var regex = new RegExp('(' + escapeRegex(escapedTerm) + ')', 'gi');
183
+ return escaped.replace(regex, '<mark>$1</mark>');
184
+ }
185
+
186
+ /**
187
+ * Process a single page and find matches
188
+ */
189
+ function processPage(pageFile, html, query) {
190
+ var parser = new DOMParser();
191
+ var doc = parser.parseFromString(html, 'text/html');
192
+ var results = [];
193
+
194
+ // Find all message blocks
195
+ var messages = doc.querySelectorAll('.message');
196
+ messages.forEach(function(msg) {
197
+ var text = msg.textContent || '';
198
+ if (text.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
199
+ var msgId = msg.id || '';
200
+ var link = getPageLinkUrl(pageFile) + (msgId ? '#' + msgId : '');
201
+
202
+ // Get a preview of the content
203
+ var contentEl = msg.querySelector('.message-content');
204
+ var preview = contentEl ? contentEl.textContent.trim() : text.trim();
205
+ preview = preview.slice(0, 300);
206
+
207
+ // Determine message role
208
+ var role = msg.classList.contains('message-user') ? 'user' : 'assistant';
209
+
210
+ results.push({
211
+ link: link,
212
+ role: role,
213
+ preview: preview,
214
+ element: msg.cloneNode(true),
215
+ pageFile: pageFile
216
+ });
217
+ }
218
+ });
219
+
220
+ return results;
221
+ }
222
+
223
+ /**
224
+ * Search within session pages
225
+ */
226
+ async function searchSessionPages(query) {
227
+ if (totalPages === 0) {
228
+ searchStatus.textContent = 'No pages to search';
229
+ return;
230
+ }
231
+
232
+ var resultsFound = 0;
233
+ var pagesSearched = 0;
234
+
235
+ // Build list of pages to fetch
236
+ var pagesToFetch = [];
237
+ for (var i = 1; i <= totalPages; i++) {
238
+ pagesToFetch.push('page-' + String(i).padStart(3, '0') + '.html');
239
+ }
240
+
241
+ // Process pages in batches
242
+ for (var i = 0; i < pagesToFetch.length && isSearching; i += BATCH_SIZE) {
243
+ var batch = pagesToFetch.slice(i, i + BATCH_SIZE);
244
+
245
+ var promises = batch.map(function(pageFile) {
246
+ return fetch(getPageFetchUrl(pageFile))
247
+ .then(function(response) {
248
+ if (!response.ok) throw new Error('Failed to fetch ' + pageFile);
249
+ return response.text();
250
+ })
251
+ .then(function(html) {
252
+ var results = processPage(pageFile, html, query);
253
+ pagesSearched++;
254
+
255
+ // Update status
256
+ searchStatus.textContent = 'Found ' + (resultsFound + results.length) + ' result(s) in ' + pagesSearched + '/' + totalPages + ' pages...';
257
+
258
+ // Display results
259
+ results.forEach(function(result) {
260
+ if (resultsFound < MAX_RESULTS) {
261
+ displayResult(result, query);
262
+ resultsFound++;
263
+ }
264
+ });
265
+
266
+ return results.length;
267
+ })
268
+ .catch(function(err) {
269
+ console.error('Error fetching page:', err);
270
+ pagesSearched++;
271
+ return 0;
272
+ });
273
+ });
274
+
275
+ await Promise.all(promises);
276
+ }
277
+
278
+ // Final status
279
+ if (isSearching) {
280
+ if (resultsFound === 0) {
281
+ searchStatus.textContent = 'No results found';
282
+ searchResults.innerHTML = '<div class="search-no-results">No matches found for "' + escapeHtml(query) + '"</div>';
283
+ } else {
284
+ searchStatus.textContent = 'Found ' + resultsFound + ' result(s) in ' + totalPages + ' pages';
285
+ if (resultsFound >= MAX_RESULTS) {
286
+ searchStatus.textContent += ' (showing first ' + MAX_RESULTS + ')';
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Search the index page (session titles and previews)
294
+ */
295
+ function searchIndexPage(query) {
296
+ var sessionCards = document.querySelectorAll('.session-card');
297
+ var resultsFound = 0;
298
+
299
+ sessionCards.forEach(function(card) {
300
+ var title = card.querySelector('.session-title');
301
+ var summary = card.querySelector('.session-summary');
302
+ var text = (title ? title.textContent : '') + ' ' + (summary ? summary.textContent : '');
303
+
304
+ if (text.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
305
+ var link = card.getAttribute('href') || '#';
306
+ var titleText = title ? title.textContent : 'Untitled Session';
307
+ var summaryText = summary ? summary.textContent : '';
308
+
309
+ displayResult({
310
+ link: link,
311
+ role: 'session',
312
+ preview: summaryText.slice(0, 200),
313
+ title: titleText,
314
+ pageFile: null
315
+ }, query);
316
+
317
+ resultsFound++;
318
+ }
319
+ });
320
+
321
+ if (resultsFound === 0) {
322
+ searchStatus.textContent = 'No results found';
323
+ searchResults.innerHTML = '<div class="search-no-results">No sessions match "' + escapeHtml(query) + '"</div>';
324
+ } else {
325
+ searchStatus.textContent = 'Found ' + resultsFound + ' session(s)';
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Display a single search result
331
+ */
332
+ function displayResult(result, query) {
333
+ var resultDiv = document.createElement('a');
334
+ resultDiv.href = result.link;
335
+ resultDiv.className = 'search-result';
336
+
337
+ var roleLabel = result.role === 'user' ? 'USER' :
338
+ result.role === 'assistant' ? 'ASSISTANT' :
339
+ 'SESSION';
340
+ var roleClass = 'search-result-role search-result-role-' + result.role;
341
+
342
+ var title = result.title || '';
343
+ var preview = result.preview || '';
344
+
345
+ // Highlight matches in preview (XSS-safe: escapes HTML before highlighting)
346
+ var highlightedPreview = highlightMatches(preview, query);
347
+
348
+ var html = '<div class="search-result-header">' +
349
+ '<span class="' + roleClass + '">' + roleLabel + '</span>';
350
+
351
+ if (result.pageFile) {
352
+ html += '<span class="search-result-page">' + escapeHtml(result.pageFile) + '</span>';
353
+ }
354
+
355
+ html += '</div>';
356
+
357
+ if (title) {
358
+ // Highlight matches in title (XSS-safe)
359
+ var highlightedTitle = highlightMatches(title, query);
360
+ html += '<div class="search-result-title">' + highlightedTitle + '</div>';
361
+ }
362
+
363
+ html += '<div class="search-result-preview">' + highlightedPreview + '</div>';
364
+
365
+ resultDiv.innerHTML = html;
366
+ searchResults.appendChild(resultDiv);
367
+ }
368
+
369
+ /**
370
+ * Main search function
371
+ */
372
+ async function performSearch(query) {
373
+ if (!query || query.length < 2) {
374
+ searchStatus.textContent = 'Type at least 2 characters to search';
375
+ searchResults.innerHTML = '';
376
+ return;
377
+ }
378
+
379
+ // Cancel any ongoing search
380
+ isSearching = false;
381
+ await new Promise(function(resolve) { setTimeout(resolve, 50); });
382
+
383
+ // Start new search
384
+ isSearching = true;
385
+ searchStatus.textContent = 'Searching...';
386
+ searchResults.innerHTML = '';
387
+ updateUrlHash(query);
388
+
389
+ if (isSessionPage) {
390
+ await searchSessionPages(query);
391
+ } else {
392
+ searchIndexPage(query);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Initialize search functionality
398
+ */
399
+ function init() {
400
+ // Check if search can work (not file:// protocol)
401
+ if (!canSearch()) {
402
+ // Hide search triggers since they won't work
403
+ var triggers = document.querySelectorAll('.search-trigger');
404
+ triggers.forEach(function(trigger) {
405
+ trigger.style.display = 'none';
406
+ });
407
+ return;
408
+ }
409
+
410
+ // Keyboard shortcut: Cmd/Ctrl + K
411
+ document.addEventListener('keydown', function(e) {
412
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
413
+ e.preventDefault();
414
+ openModal();
415
+ }
416
+ });
417
+
418
+ // Search trigger button click
419
+ var triggers = document.querySelectorAll('.search-trigger');
420
+ triggers.forEach(function(trigger) {
421
+ trigger.addEventListener('click', function(e) {
422
+ e.preventDefault();
423
+ openModal();
424
+ });
425
+ });
426
+
427
+ // Check for search in URL hash on page load
428
+ if (window.location.hash.startsWith('#search=')) {
429
+ var query = decodeURIComponent(window.location.hash.substring(8));
430
+ if (query) {
431
+ // Delay to ensure page is ready
432
+ setTimeout(function() {
433
+ openModal(query);
434
+ }, 100);
435
+ }
436
+ }
437
+ }
438
+
439
+ // Initialize when DOM is ready
440
+ if (document.readyState === 'loading') {
441
+ document.addEventListener('DOMContentLoaded', init);
442
+ } else {
443
+ init();
444
+ }
445
+ })();