// ==UserScript==
// @name DeepWiki Button Enhanced
// @namespace https://github.com/zhsama/deepwiki.git
// @version 1.0
// @description Adds a DeepWiki button to GitHub repository pages, linking to deepwiki.com/{user}/{repo}. Combines features and optimizes previous scripts.
// @author zhsama
// @match https://github.com/*/*
// @grant none
// @license MIT
// @icon https://deepwiki.com/icon.png?66aaf51e0e68c818
// @supportURL https://github.com/zhsama/deepwiki
// @homepageURL https://github.com/zhsama/deepwiki
// ==/UserScript==
(function () {
'use strict';
const BUTTON_ID = 'deepwiki-button-enhanced';
let lastUrl = location.href; // Track URL for SPA navigation changes
// --- Logging ---
const log = (...args) => console.log('[DeepWiki Button]', ...args);
const errorLog = (...args) => console.error('[DeepWiki Button]', ...args);
// --- Page Detection ---
/**
* Checks if the current page is a GitHub repository page (not settings, issues list, etc.)
* @returns {boolean} True if it's a repository page, false otherwise.
*/
function isRepoPage() {
try {
// Basic check: path must have at least user/repo
const pathParts = window.location.pathname.split('/').filter(Boolean);
if (pathParts.length < 2) return false;
// Exclude common non-repo pages that match the basic path structure
const nonRepoPaths = [
'/settings', '/issues', '/pulls', '/projects', '/actions',
'/security', '/pulse', '/graphs', '/search', '/marketplace',
'/explore', '/topics', '/trending', '/sponsors', '/new',
'/organizations/', '/codespaces'
];
if (nonRepoPaths.some(p => window.location.pathname.includes(p))) {
// Allow issues/pulls detail pages
if ((window.location.pathname.includes('/issues/') || window.location.pathname.includes('/pull/')) && pathParts.length > 3) {
// It's an issue/PR detail page, potentially show button? For now, let's keep it strict to repo main/code view
// return true; // Uncomment if you want the button on issue/PR details
return false; // Keep button only on repo views for now
}
return false;
}
// Check for main repo container elements (more reliable)
const mainContentSelectors = [
'main#js-repo-pjax-container', // Older structure
'div[data-pjax="#repo-content-pjax-container"]', // Newer structure
'.repohead', // Repo header element
'.repository-content' // Main content area
];
if (mainContentSelectors.some(sel => document.querySelector(sel))) {
return true;
}
// Fallback: If path has user/repo and not excluded, assume it's a repo page.
// Be cautious with this fallback.
// log("Falling back to path check for repo page detection.");
// return true;
return false; // Stricter check - rely on selectors
} catch (e) {
errorLog('Error checking if it is a repo page:', e);
return false;
}
}
/**
* Extracts username and repository name from the current URL.
* @returns {{user: string, repo: string} | null} Object with user/repo or null if not found.
*/
function getUserAndRepo() {
try {
const pathParts = window.location.pathname.split('/').filter(part => part.length > 0);
if (pathParts.length >= 2) {
// Handle cases like /user/repo/tree/branch or /user/repo/blob/branch/file
return {
user: pathParts[0],
repo: pathParts[1]
};
}
} catch (e) {
errorLog('Error getting user and repo info:', e);
}
return null;
}
// --- Button Creation ---
/**
* Creates the SVG icon element for the DeepWiki button.
* @returns {SVGSVGElement} The SVG element.
*/
function createSVGIconElement() {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
// Using a simpler, cleaner icon representation if possible, or the provided one.
// Let's use the provided complex one for now.
svg.setAttribute('class', 'octicon'); // Use GitHub's icon class if possible
svg.setAttribute('width', '16');
svg.setAttribute('height', '16');
svg.setAttribute('viewBox', '110 110 460 500'); // As provided in script 2
svg.setAttribute('style', 'margin-right: 4px; vertical-align: text-bottom; color: var(--color-accent-fg);'); // Use CSS variable for color
// Paths from Script 2 (ensure they are valid and display correctly)
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>`;
return svg;
}
/**
* Creates the DeepWiki button element.
* @param {string} user - GitHub username.
* @param {string} repo - GitHub repository name.
* @returns {HTMLAnchorElement | null} The button element or null on error.
*/
function createDeepWikiButton(user, repo) {
try {
const deepwikiUrl = `https://deepwiki.com/${user}/${repo}`;
// Find a template button to mimic style (prefer action buttons)
const selectors = [
'.BtnGroup button', // Button group (e.g., Watch/Fork/Star)
'.BtnGroup a', // Sometimes links are used in groups
'a.btn', // General buttons
'button.btn',
'[data-hotkey="w"]', // Watch button hotkey
'[data-hotkey="s"]', // Star button hotkey
'[data-ga-click*="Fork"]' // Fork button analytics attrib
];
let templateButton = null;
for (const selector of selectors) {
// Find a visible button that isn't the deepwiki one itself
templateButton = Array.from(document.querySelectorAll(selector)).find(btn =>
btn.offsetParent !== null && !btn.id.startsWith('deepwiki-button') && !btn.closest(`#${BUTTON_ID}`)
);
if (templateButton) break;
}
// Create button element
const button = document.createElement('a');
button.href = deepwikiUrl;
button.id = BUTTON_ID;
button.target = '_blank';
button.rel = 'noopener noreferrer';
button.title = `View Wiki for ${user}/${repo} on DeepWiki`;
button.setAttribute('data-user', user);
button.setAttribute('data-repo', repo);
button.setAttribute('aria-label', `View Wiki for ${user}/${repo} on DeepWiki`);
button.style.textDecoration = 'none'; // Ensure no underline
// Apply styles
if (templateButton) {
// Mimic classes, filtering out state-specific ones
const classNames = Array.from(templateButton.classList).filter(cls =>
!['selected', 'disabled', 'tooltipped', 'js-selected-navigation-item'].includes(cls) &&
!cls.startsWith('BtnGroup') // Avoid BtnGroup item specific styles if adding outside a group
);
// Ensure basic button classes are present if missed
if (!classNames.some(cls => cls.startsWith('btn') || cls.startsWith('Btn'))) {
classNames.push('btn'); // Add basic button class
}
// Add size class if template had one (e.g., btn-sm)
if (Array.from(templateButton.classList).some(cls => cls.includes('-sm'))) {
if (!classNames.includes('btn-sm')) classNames.push('btn-sm');
} else if (Array.from(templateButton.classList).some(cls => cls.includes('-large'))) {
if (!classNames.includes('btn-large')) classNames.push('btn-large');
}
button.className = classNames.join(' ');
log('Mimicking styles from:', templateButton, 'Resulting classes:', button.className);
} else {
// Fallback basic styling if no template found
log('No template button found, applying fallback styles.');
button.className = 'btn btn-sm'; // Default to small button
// Optional: Add minimal inline styles if needed, but prefer classes
// button.style.backgroundColor = '#f6f8fa';
// button.style.border = '1px solid rgba(27,31,36,0.15)';
// button.style.borderRadius = '6px';
// button.style.color = '#24292f';
// button.style.padding = '3px 12px';
// button.style.fontSize = '12px';
// button.style.fontWeight = '500';
// button.style.lineHeight = '20px';
}
// Add SVG icon (inside the button)
const svgIcon = createSVGIconElement();
button.appendChild(svgIcon);
// Add text (inside the button)
const text = document.createTextNode(' DeepWiki'); // Add space for clarity
button.appendChild(text);
// Add click tracking/logging
button.addEventListener('click', function (e) {
log(`DeepWiki button clicked for: ${user}/${repo}`);
// Optional: Add analytics tracking here if desired
});
return button;
} catch (e) {
errorLog('Error creating DeepWiki button:', e);
return null;
}
}
// --- Button Insertion ---
/**
* Finds the best location and inserts the DeepWiki button.
*/
function addDeepWikiButton() {
// 1. Pre-checks
if (!isRepoPage()) {
// log('Not a repository page, skipping button add.');
return;
}
if (document.getElementById(BUTTON_ID)) {
// log('DeepWiki button already exists.');
return;
}
const userAndRepo = getUserAndRepo();
if (!userAndRepo) {
errorLog('Could not extract user/repo info, cannot add button.');
return;
}
// 2. Create the button
const deepWikiButton = createDeepWikiButton(userAndRepo.user, userAndRepo.repo);
if (!deepWikiButton) {
errorLog('Button creation failed.');
return;
}
// 3. Find insertion point (try multiple locations)
// Prioritize the specific target: repository-details-container div's ul
const targetSelectors = [
// --- Primary Target (Specific container requested) ---
'#repository-details-container ul', // Specific container requested
// --- Secondary Targets (UL elements for list items) ---
'.pagehead-actions ul', // Older structure action list
'.AppHeader-context-full nav > ul', // Newest header nav
'nav[aria-label="Repository"] ul', // Repo navigation tabs
// --- Fallback Targets (Other areas) ---
'.gh-header-actions', // Newer structure action area
'.repository-content .Box-header .d-flex .BtnGroup',
'#repository-container-header .BtnGroup',
'.file-navigation', // File browser header
'.Layout-sidebar' // Right sidebar
];
let targetElement = null;
let insertionMethod = 'appendChild'; // Default: add to the end
for (const selector of targetSelectors) {
targetElement = document.querySelector(selector);
if (targetElement) {
log(`Found target element using selector: ${selector}, tagName: ${targetElement.tagName}`);
// If we found the specific target we're looking for, break immediately
if (selector === '#repository-details-container ul') {
log('Found the specific target container requested');
}
break; // Stop searching once a target is found
}
}
// 4. Insert the button
if (targetElement) {
try {
let elementToInsert;
// If target is a UL, always add as a list item
if (targetElement.tagName === 'UL') {
log('Target is a UL element, creating list item for DeepWiki button');
const li = document.createElement('li');
// Try to copy classes from sibling LIs for better alignment/styling in lists
const siblingLi = targetElement.querySelector('li:last-child');
if (siblingLi) {
li.className = siblingLi.className;
log(`Copied classes from sibling LI: ${siblingLi.className}`);
}
li.style.marginLeft = '8px'; // Ensure some space if classes don't provide it
li.appendChild(deepWikiButton);
elementToInsert = li;
} else if (targetElement.classList.contains('BtnGroup')) {
// If inserting into a button group, don't wrap, just append
elementToInsert = deepWikiButton;
} else {
// For any other target, add directly
elementToInsert = deepWikiButton;
}
// Insert the element
targetElement.appendChild(elementToInsert);
log(`Successfully added DeepWiki button for ${userAndRepo.user}/${userAndRepo.repo}`);
if (targetElement.closest('#repository-details-container')) {
log('DeepWiki button was added to the repository-details-container as requested');
}
} catch (e) {
errorLog('Error inserting button into target element:', e, targetElement);
}
} else {
log('Could not find a suitable location to add the DeepWiki button.');
}
}
// --- Dynamic Loading Handling ---
let observer = null;
let mutationDebounceTimeout = null;
/**
* Sets up a MutationObserver to watch for DOM changes and URL changes (via DOM).
*/
function setupObserver() {
if (observer) {
// log("Observer already running.");
return; // Don't set up multiple observers
}
try {
observer = new MutationObserver((mutations) => {
// Check if URL changed significantly (indicating SPA navigation)
if (location.href !== lastUrl) {
log(`URL changed from ${lastUrl} to ${location.href}`);
lastUrl = location.href;
// Clear any existing button immediately on URL change before adding new one
const existingButton = document.getElementById(BUTTON_ID);
if (existingButton) {
existingButton.remove();
log('Removed old button due to URL change.');
}
// Re-run add button logic after a short delay for the page to settle
clearTimeout(mutationDebounceTimeout); // Clear previous debounce timer
mutationDebounceTimeout = setTimeout(addDeepWikiButton, 300); // Debounce checks
} else {
// URL didn't change, but DOM did. Check if button *should* be there but isn't.
// This handles cases where parts of the header re-render without full navigation.
if (isRepoPage() && !document.getElementById(BUTTON_ID)) {
clearTimeout(mutationDebounceTimeout); // Clear previous debounce timer
mutationDebounceTimeout = setTimeout(addDeepWikiButton, 500); // Longer debounce for general mutations
}
}
// Optimization: Disconnect observer if we are definitely not on a repo page?
// Could add: if (!isRepoPage() && observer) { observer.disconnect(); observer = null; log('Disconnected observer, not repo page'); }
// But re-connecting might be tricky, so let's keep it simple for now.
});
observer.observe(document.body, {
childList: true, // Watch for adding/removing nodes
subtree: true // Watch descendants too
});
log('MutationObserver set up successfully.');
} catch (e) {
errorLog('Failed to set up MutationObserver:', e);
}
}
// --- Initialization ---
/**
* Initializes the script: adds the button and sets up the observer.
*/
function init() {
log('Initializing DeepWiki Button script...');
// Initial attempt to add the button
addDeepWikiButton();
// Retry mechanism for initial load race conditions
// Use increasing delays
setTimeout(addDeepWikiButton, 500);
setTimeout(addDeepWikiButton, 1500); // Longer delay
setTimeout(addDeepWikiButton, 3000); // Even longer
// Set up the observer to handle SPA navigation and dynamic content
setupObserver();
// Optional: Listen to popstate for back/forward navigation (though observer often catches this too)
window.addEventListener('popstate', () => {
log('popstate event detected');
// Give observer a chance first, but force check after a delay
setTimeout(addDeepWikiButton, 200);
});
}
// --- Run ---
// Use DOMContentLoaded for initial run, but also check readyState for already loaded pages.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// The document is already loaded ('interactive' or 'complete')
init();
}
})();