DeepWiki Button Enhanced

Adds a DeepWiki button to GitHub repository pages, linking to deepwiki.com/{user}/{repo}. Combines features and optimizes previous scripts.

  1. // ==UserScript==
  2. // @name DeepWiki Button Enhanced
  3. // @namespace https://github.com/zhsama/deepwiki.git
  4. // @version 1.0
  5. // @description Adds a DeepWiki button to GitHub repository pages, linking to deepwiki.com/{user}/{repo}. Combines features and optimizes previous scripts.
  6. // @author zhsama
  7. // @match https://github.com/*/*
  8. // @grant none
  9. // @license MIT
  10. // @icon https://deepwiki.com/icon.png?66aaf51e0e68c818
  11. // @supportURL https://github.com/zhsama/deepwiki
  12. // @homepageURL https://github.com/zhsama/deepwiki
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const BUTTON_ID = 'deepwiki-button-enhanced';
  19. let lastUrl = location.href; // Track URL for SPA navigation changes
  20.  
  21. // --- Logging ---
  22. const log = (...args) => console.log('[DeepWiki Button]', ...args);
  23. const errorLog = (...args) => console.error('[DeepWiki Button]', ...args);
  24.  
  25. // --- Page Detection ---
  26. /**
  27. * Checks if the current page is a GitHub repository page (not settings, issues list, etc.)
  28. * @returns {boolean} True if it's a repository page, false otherwise.
  29. */
  30. function isRepoPage() {
  31. try {
  32. // Basic check: path must have at least user/repo
  33. const pathParts = window.location.pathname.split('/').filter(Boolean);
  34. if (pathParts.length < 2) return false;
  35.  
  36. // Exclude common non-repo pages that match the basic path structure
  37. const nonRepoPaths = [
  38. '/settings', '/issues', '/pulls', '/projects', '/actions',
  39. '/security', '/pulse', '/graphs', '/search', '/marketplace',
  40. '/explore', '/topics', '/trending', '/sponsors', '/new',
  41. '/organizations/', '/codespaces'
  42. ];
  43. if (nonRepoPaths.some(p => window.location.pathname.includes(p))) {
  44. // Allow issues/pulls detail pages
  45. if ((window.location.pathname.includes('/issues/') || window.location.pathname.includes('/pull/')) && pathParts.length > 3) {
  46. // It's an issue/PR detail page, potentially show button? For now, let's keep it strict to repo main/code view
  47. // return true; // Uncomment if you want the button on issue/PR details
  48. return false; // Keep button only on repo views for now
  49. }
  50. return false;
  51. }
  52.  
  53. // Check for main repo container elements (more reliable)
  54. const mainContentSelectors = [
  55. 'main#js-repo-pjax-container', // Older structure
  56. 'div[data-pjax="#repo-content-pjax-container"]', // Newer structure
  57. '.repohead', // Repo header element
  58. '.repository-content' // Main content area
  59. ];
  60. if (mainContentSelectors.some(sel => document.querySelector(sel))) {
  61. return true;
  62. }
  63.  
  64.  
  65. // Fallback: If path has user/repo and not excluded, assume it's a repo page.
  66. // Be cautious with this fallback.
  67. // log("Falling back to path check for repo page detection.");
  68. // return true;
  69.  
  70. return false; // Stricter check - rely on selectors
  71.  
  72.  
  73. } catch (e) {
  74. errorLog('Error checking if it is a repo page:', e);
  75. return false;
  76. }
  77. }
  78.  
  79. /**
  80. * Extracts username and repository name from the current URL.
  81. * @returns {{user: string, repo: string} | null} Object with user/repo or null if not found.
  82. */
  83. function getUserAndRepo() {
  84. try {
  85. const pathParts = window.location.pathname.split('/').filter(part => part.length > 0);
  86. if (pathParts.length >= 2) {
  87. // Handle cases like /user/repo/tree/branch or /user/repo/blob/branch/file
  88. return {
  89. user: pathParts[0],
  90. repo: pathParts[1]
  91. };
  92. }
  93. } catch (e) {
  94. errorLog('Error getting user and repo info:', e);
  95. }
  96. return null;
  97. }
  98.  
  99. // --- Button Creation ---
  100. /**
  101. * Creates the SVG icon element for the DeepWiki button.
  102. * @returns {SVGSVGElement} The SVG element.
  103. */
  104. function createSVGIconElement() {
  105. const svgNS = 'http://www.w3.org/2000/svg';
  106. const svg = document.createElementNS(svgNS, 'svg');
  107. // Using a simpler, cleaner icon representation if possible, or the provided one.
  108. // Let's use the provided complex one for now.
  109. svg.setAttribute('class', 'octicon'); // Use GitHub's icon class if possible
  110. svg.setAttribute('width', '16');
  111. svg.setAttribute('height', '16');
  112. svg.setAttribute('viewBox', '110 110 460 500'); // As provided in script 2
  113. svg.setAttribute('style', 'margin-right: 4px; vertical-align: text-bottom; color: var(--color-accent-fg);'); // Use CSS variable for color
  114. // Paths from Script 2 (ensure they are valid and display correctly)
  115. svg.innerHTML = `<path style="fill:#21c19a" d="M418.73,332.37c9.84-5.68,22.07-5.68,31.91,0l25.49,14.71c.82.48,1.69.8,2.58,1.06.19.06.37.11.55.16.87.21,1.76.34,2.65.35.04,0,.08.02.13.02.1,0,.19-.03.29-.04.83-.02,1.64-.13,2.45-.32.14-.03.28-.05.42-.09.87-.24,1.7-.59,2.5-1.03.08-.04.17-.06.25-.1l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-.08.04-.13.11-.2.16-.78.48-1.51,1.02-2.15,1.66-.1.1-.18.21-.28.31-.57.6-1.08,1.26-1.51,1.97-.07.12-.15.22-.22.34-.44.77-.77,1.6-1.03,2.47-.05.19-.1.37-.14.56-.22.89-.37,1.81-.37,2.76v29.43c0,11.36-6.11,21.95-15.95,27.63-9.84,5.68-22.06,5.68-31.91,0l-25.49-14.71c-.82-.48-1.69-.8-2.57-1.06-.19-.06-.37-.11-.56-.16-.88-.21-1.76-.34-2.65-.34-.13,0-.26.02-.4.02-.84.02-1.66.13-2.47.32-.13.03-.27.05-.4.09-.87.24-1.71.6-2.51,1.04-.08.04-.16.06-.24.1l-50.97,29.43c-3.65-2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22l50.97,29.43c.08.04.17.06.24.1.8.44,1.64.79,2.5,1.03.14.04.28.06.42.09.81.19,1.62.3,2.45.32.1,0,.19.04.29.04.04,0,.08-.02.13-.02.89,0,1.77-.13,2.65-.35.19-.04.37-.1.56-.16.88-.26,1.75-.59,2.58-1.06l25.49-14.71c9.84-5.68,22.06-5.68,31.91,0,9.84,5.68,15.95,16.27,15.95,27.63v29.43c0,.95.15,1.87.37,2.76.05.19.09.37.14.56.25.86.59,1.69,1.03,2.47.07.12.15.22.22.34.43.71.94,1.37,1.51,1.97.1.1.18.21.28.31.65.63,1.37,1.18,2.15,1.66.07.04.13.11.2.16l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-.08-.04-.16-.06-.24-.1-.8-.44-1.64-.8-2.51-1.04-.13-.04-.26-.05-.39-.09-.82-.2-1.65-.31-2.49-.33-.13,0-.25-.02-.38-.02-.89,0-1.78.13-2.66.35-.18.04-.36.1-.54.15-.88.26-1.75.59-2.58,1.07l-25.49,14.72c-9.84,5.68-22.07,5.68-31.9,0-9.84-5.68-15.95-16.27-15.95-27.63s6.11-21.95,15.95-27.63Z"></path><path style="fill:#3969ca" d="M141.09,317.65l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c.08-.04.13-.11.2-.16.78-.48,1.51-1.02,2.15-1.66.1-.1.18-.21.28-.31.57-.6,1.08-1.26,1.51-1.97.07-.12.15-.22.22-.34.44-.77.77-1.6,1.03-2.47.05-.19.1-.37.14-.56.22-.89.37-1.81.37-2.76v-29.43c0-11.36,6.11-21.95,15.96-27.63s22.06-5.68,31.91,0l25.49,14.71c.82.48,1.69.8,2.57,1.06.19.06.37.11.56.16.87.21,1.76.34,2.64.35.04,0,.09.02.13.02.1,0,.19-.04.29-.04.83-.02,1.65-.13,2.45-.32.14-.03.28-.05.41-.09.87-.24,1.71-.6,2.51-1.04.08-.04.16-.06.24-.1l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-.08.04-.13.11-.2.16-.78.48-1.51,1.02-2.15,1.66-.1.1-.18.21-.28.31-.57.6-1.08,1.26-1.51,1.97-.07.12-.15.22-.22.34-.44.77-.77,1.6-1.03,2.47-.05.19-.1.37-.14.56-.22.89-.37,1.81-.37,2.76v29.43c0,11.36-6.11,21.95-15.95,27.63-9.84,5.68-22.07,5.68-31.91,0l-25.49-14.71c-.82-.48-1.69-.8-2.58-1.06-.19-.06-.37-.11-.55-.16-.88-.21-1.76-.34-2.65-.35-.13,0-.26.02-.4.02-.83.02-1.66.13-2.47.32-.13.03-.27.05-.4.09-.87.24-1.71.6-2.51,1.04-.08.04-.16.06-.24.1l-50.97,29.43c-3.65-2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22Z"></path><path style="fill:#0294de" d="M396.88,484.35l-50.97-29.43c-.08-.04-.17-.06-.24-.1-.8-.44-1.64-.79-2.51-1.03-.14-.04-.27-.06-.41-.09-.81-.19-1.64-.3-2.47-.32-.13,0-.26-.02-.39-.02-.89,0-1.78.13-2.66.35-.18.04-.36.1-.54.15-.88.26-1.76.59-2.58,1.07l-25.49,14.72c-9.84,5.68-22.06,5.68-31.9,0-9.84-5.68-15.96-16.27-15.96-27.63v-29.43c0-.95-.15-1.87-.37-2.76-.05-.19-.09-.37-.14-.56-.25-.86-.59-1.69-1.03-2.47-.07-.12-.15.22-.22.34-.43-.71-.94-1.37-1.51-1.97-.1-.1-.18-.21-.28-.31-.65-.63-1.37-1.18-2.15-1.66-.07-.04-.13-.11-.2-.16l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-3.65,2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22l50.97,29.43c.08.04.17.06.25.1.8.44,1.63.79,2.5,1.03.14.04.29.06.43.09.8.19,1.61.3,2.43.32.1,0,.2.04.3.04.04,0,.09-.02.13-.02.88,0,1.77-.13,2.64-.34.19-.04.37-.1.56-.16.88-.26,1.75-.59,2.57-1.06l25.49-14.71c9.84-5.68,22.06-5.68,31.91,0,9.84,5.68,15.95,16.27,15.95,27.63v29.43c0,.95.15,1.87.37,2.76.05.19.09.37.14.56.25.86.59,1.69,1.03,2.47.07.12.15.22.22.34.43.71.94,1.37,1.51,1.97.1.1.18.21.28.31.65.63,1.37,1.18,2.15,1.66.07.04.13.11.2.16l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22Z"></path>`;
  116. return svg;
  117. }
  118.  
  119. /**
  120. * Creates the DeepWiki button element.
  121. * @param {string} user - GitHub username.
  122. * @param {string} repo - GitHub repository name.
  123. * @returns {HTMLAnchorElement | null} The button element or null on error.
  124. */
  125. function createDeepWikiButton(user, repo) {
  126. try {
  127. const deepwikiUrl = `https://deepwiki.com/${user}/${repo}`;
  128.  
  129. // Find a template button to mimic style (prefer action buttons)
  130. const selectors = [
  131. '.BtnGroup button', // Button group (e.g., Watch/Fork/Star)
  132. '.BtnGroup a', // Sometimes links are used in groups
  133. 'a.btn', // General buttons
  134. 'button.btn',
  135. '[data-hotkey="w"]', // Watch button hotkey
  136. '[data-hotkey="s"]', // Star button hotkey
  137. '[data-ga-click*="Fork"]' // Fork button analytics attrib
  138. ];
  139. let templateButton = null;
  140. for (const selector of selectors) {
  141. // Find a visible button that isn't the deepwiki one itself
  142. templateButton = Array.from(document.querySelectorAll(selector)).find(btn =>
  143. btn.offsetParent !== null && !btn.id.startsWith('deepwiki-button') && !btn.closest(`#${BUTTON_ID}`)
  144. );
  145. if (templateButton) break;
  146. }
  147.  
  148.  
  149. // Create button element
  150. const button = document.createElement('a');
  151. button.href = deepwikiUrl;
  152. button.id = BUTTON_ID;
  153. button.target = '_blank';
  154. button.rel = 'noopener noreferrer';
  155. button.title = `View Wiki for ${user}/${repo} on DeepWiki`;
  156. button.setAttribute('data-user', user);
  157. button.setAttribute('data-repo', repo);
  158. button.setAttribute('aria-label', `View Wiki for ${user}/${repo} on DeepWiki`);
  159. button.style.textDecoration = 'none'; // Ensure no underline
  160.  
  161. // Apply styles
  162. if (templateButton) {
  163. // Mimic classes, filtering out state-specific ones
  164. const classNames = Array.from(templateButton.classList).filter(cls =>
  165. !['selected', 'disabled', 'tooltipped', 'js-selected-navigation-item'].includes(cls) &&
  166. !cls.startsWith('BtnGroup') // Avoid BtnGroup item specific styles if adding outside a group
  167. );
  168. // Ensure basic button classes are present if missed
  169. if (!classNames.some(cls => cls.startsWith('btn') || cls.startsWith('Btn'))) {
  170. classNames.push('btn'); // Add basic button class
  171. }
  172. // Add size class if template had one (e.g., btn-sm)
  173. if (Array.from(templateButton.classList).some(cls => cls.includes('-sm'))) {
  174. if (!classNames.includes('btn-sm')) classNames.push('btn-sm');
  175. } else if (Array.from(templateButton.classList).some(cls => cls.includes('-large'))) {
  176. if (!classNames.includes('btn-large')) classNames.push('btn-large');
  177. }
  178.  
  179.  
  180. button.className = classNames.join(' ');
  181. log('Mimicking styles from:', templateButton, 'Resulting classes:', button.className);
  182.  
  183.  
  184. } else {
  185. // Fallback basic styling if no template found
  186. log('No template button found, applying fallback styles.');
  187. button.className = 'btn btn-sm'; // Default to small button
  188. // Optional: Add minimal inline styles if needed, but prefer classes
  189. // button.style.backgroundColor = '#f6f8fa';
  190. // button.style.border = '1px solid rgba(27,31,36,0.15)';
  191. // button.style.borderRadius = '6px';
  192. // button.style.color = '#24292f';
  193. // button.style.padding = '3px 12px';
  194. // button.style.fontSize = '12px';
  195. // button.style.fontWeight = '500';
  196. // button.style.lineHeight = '20px';
  197. }
  198.  
  199.  
  200. // Add SVG icon (inside the button)
  201. const svgIcon = createSVGIconElement();
  202. button.appendChild(svgIcon);
  203.  
  204. // Add text (inside the button)
  205. const text = document.createTextNode(' DeepWiki'); // Add space for clarity
  206. button.appendChild(text);
  207.  
  208.  
  209. // Add click tracking/logging
  210. button.addEventListener('click', function (e) {
  211. log(`DeepWiki button clicked for: ${user}/${repo}`);
  212. // Optional: Add analytics tracking here if desired
  213. });
  214.  
  215. return button;
  216. } catch (e) {
  217. errorLog('Error creating DeepWiki button:', e);
  218. return null;
  219. }
  220. }
  221.  
  222. // --- Button Insertion ---
  223. /**
  224. * Finds the best location and inserts the DeepWiki button.
  225. */
  226. function addDeepWikiButton() {
  227. // 1. Pre-checks
  228. if (!isRepoPage()) {
  229. // log('Not a repository page, skipping button add.');
  230. return;
  231. }
  232. if (document.getElementById(BUTTON_ID)) {
  233. // log('DeepWiki button already exists.');
  234. return;
  235. }
  236.  
  237. const userAndRepo = getUserAndRepo();
  238. if (!userAndRepo) {
  239. errorLog('Could not extract user/repo info, cannot add button.');
  240. return;
  241. }
  242.  
  243. // 2. Create the button
  244. const deepWikiButton = createDeepWikiButton(userAndRepo.user, userAndRepo.repo);
  245. if (!deepWikiButton) {
  246. errorLog('Button creation failed.');
  247. return;
  248. }
  249.  
  250. // 3. Find insertion point (try multiple locations)
  251. // Prioritize the specific target: repository-details-container div's ul
  252. const targetSelectors = [
  253. // --- Primary Target (Specific container requested) ---
  254. '#repository-details-container ul', // Specific container requested
  255. // --- Secondary Targets (UL elements for list items) ---
  256. '.pagehead-actions ul', // Older structure action list
  257. '.AppHeader-context-full nav > ul', // Newest header nav
  258. 'nav[aria-label="Repository"] ul', // Repo navigation tabs
  259. // --- Fallback Targets (Other areas) ---
  260. '.gh-header-actions', // Newer structure action area
  261. '.repository-content .Box-header .d-flex .BtnGroup',
  262. '#repository-container-header .BtnGroup',
  263. '.file-navigation', // File browser header
  264. '.Layout-sidebar' // Right sidebar
  265. ];
  266.  
  267. let targetElement = null;
  268. let insertionMethod = 'appendChild'; // Default: add to the end
  269.  
  270. for (const selector of targetSelectors) {
  271. targetElement = document.querySelector(selector);
  272. if (targetElement) {
  273. log(`Found target element using selector: ${selector}, tagName: ${targetElement.tagName}`);
  274. // If we found the specific target we're looking for, break immediately
  275. if (selector === '#repository-details-container ul') {
  276. log('Found the specific target container requested');
  277. }
  278. break; // Stop searching once a target is found
  279. }
  280. }
  281.  
  282.  
  283. // 4. Insert the button
  284. if (targetElement) {
  285. try {
  286. let elementToInsert;
  287.  
  288. // If target is a UL, always add as a list item
  289. if (targetElement.tagName === 'UL') {
  290. log('Target is a UL element, creating list item for DeepWiki button');
  291. const li = document.createElement('li');
  292. // Try to copy classes from sibling LIs for better alignment/styling in lists
  293. const siblingLi = targetElement.querySelector('li:last-child');
  294. if (siblingLi) {
  295. li.className = siblingLi.className;
  296. log(`Copied classes from sibling LI: ${siblingLi.className}`);
  297. }
  298. li.style.marginLeft = '8px'; // Ensure some space if classes don't provide it
  299. li.appendChild(deepWikiButton);
  300. elementToInsert = li;
  301. } else if (targetElement.classList.contains('BtnGroup')) {
  302. // If inserting into a button group, don't wrap, just append
  303. elementToInsert = deepWikiButton;
  304. } else {
  305. // For any other target, add directly
  306. elementToInsert = deepWikiButton;
  307. }
  308.  
  309. // Insert the element
  310. targetElement.appendChild(elementToInsert);
  311.  
  312. log(`Successfully added DeepWiki button for ${userAndRepo.user}/${userAndRepo.repo}`);
  313. if (targetElement.closest('#repository-details-container')) {
  314. log('DeepWiki button was added to the repository-details-container as requested');
  315. }
  316. } catch (e) {
  317. errorLog('Error inserting button into target element:', e, targetElement);
  318. }
  319. } else {
  320. log('Could not find a suitable location to add the DeepWiki button.');
  321. }
  322. }
  323.  
  324. // --- Dynamic Loading Handling ---
  325. let observer = null;
  326. let mutationDebounceTimeout = null;
  327.  
  328. /**
  329. * Sets up a MutationObserver to watch for DOM changes and URL changes (via DOM).
  330. */
  331. function setupObserver() {
  332. if (observer) {
  333. // log("Observer already running.");
  334. return; // Don't set up multiple observers
  335. }
  336.  
  337. try {
  338. observer = new MutationObserver((mutations) => {
  339. // Check if URL changed significantly (indicating SPA navigation)
  340. if (location.href !== lastUrl) {
  341. log(`URL changed from ${lastUrl} to ${location.href}`);
  342. lastUrl = location.href;
  343. // Clear any existing button immediately on URL change before adding new one
  344. const existingButton = document.getElementById(BUTTON_ID);
  345. if (existingButton) {
  346. existingButton.remove();
  347. log('Removed old button due to URL change.');
  348. }
  349.  
  350. // Re-run add button logic after a short delay for the page to settle
  351. clearTimeout(mutationDebounceTimeout); // Clear previous debounce timer
  352. mutationDebounceTimeout = setTimeout(addDeepWikiButton, 300); // Debounce checks
  353. } else {
  354. // URL didn't change, but DOM did. Check if button *should* be there but isn't.
  355. // This handles cases where parts of the header re-render without full navigation.
  356. if (isRepoPage() && !document.getElementById(BUTTON_ID)) {
  357. clearTimeout(mutationDebounceTimeout); // Clear previous debounce timer
  358. mutationDebounceTimeout = setTimeout(addDeepWikiButton, 500); // Longer debounce for general mutations
  359. }
  360. }
  361.  
  362. // Optimization: Disconnect observer if we are definitely not on a repo page?
  363. // Could add: if (!isRepoPage() && observer) { observer.disconnect(); observer = null; log('Disconnected observer, not repo page'); }
  364. // But re-connecting might be tricky, so let's keep it simple for now.
  365. });
  366.  
  367. observer.observe(document.body, {
  368. childList: true, // Watch for adding/removing nodes
  369. subtree: true // Watch descendants too
  370. });
  371. log('MutationObserver set up successfully.');
  372. } catch (e) {
  373. errorLog('Failed to set up MutationObserver:', e);
  374. }
  375. }
  376.  
  377. // --- Initialization ---
  378. /**
  379. * Initializes the script: adds the button and sets up the observer.
  380. */
  381. function init() {
  382. log('Initializing DeepWiki Button script...');
  383.  
  384. // Initial attempt to add the button
  385. addDeepWikiButton();
  386.  
  387. // Retry mechanism for initial load race conditions
  388. // Use increasing delays
  389. setTimeout(addDeepWikiButton, 500);
  390. setTimeout(addDeepWikiButton, 1500); // Longer delay
  391. setTimeout(addDeepWikiButton, 3000); // Even longer
  392.  
  393.  
  394. // Set up the observer to handle SPA navigation and dynamic content
  395. setupObserver();
  396.  
  397. // Optional: Listen to popstate for back/forward navigation (though observer often catches this too)
  398. window.addEventListener('popstate', () => {
  399. log('popstate event detected');
  400. // Give observer a chance first, but force check after a delay
  401. setTimeout(addDeepWikiButton, 200);
  402. });
  403.  
  404. }
  405.  
  406. // --- Run ---
  407. // Use DOMContentLoaded for initial run, but also check readyState for already loaded pages.
  408. if (document.readyState === 'loading') {
  409. document.addEventListener('DOMContentLoaded', init);
  410. } else {
  411. // The document is already loaded ('interactive' or 'complete')
  412. init();
  413. }
  414.  
  415. })();