commit 084ce6d6d6801abd5913145f3b3787d719a9e35c Author: Lord_Devi Date: Thu Nov 21 03:51:48 2024 -0500 First commit. diff --git a/content-script.js b/content-script.js new file mode 100644 index 0000000..8272626 --- /dev/null +++ b/content-script.js @@ -0,0 +1,143 @@ +(function() { + function extractKeywords() { + // Find the keyword grid + const keywordGrid = document.querySelector('[role="grid"][aria-label="Keyword ideas"]'); + if (!keywordGrid) { + alert('Could not find the keyword ideas grid. Please make sure you are on the correct page.'); + return; + } + + // Initialize CSV content based on includeHeaders preference + let csvContent = window.includeHeaders ? 'Keyword,Average Monthly Searches,Source\r\n' : ''; + + // Keep track of processed keywords to prevent duplicates + const processedKeywords = new Set(); + + // Function to clean search volume + function cleanSearchVolume(volume) { + // Remove any commas from the number + return volume.replace(/,/g, ''); + } + + // Function to process rows between start and end indices + function processRowsInRange(startIdx, endIdx, source) { + const rows = document.querySelectorAll('.particle-table-row'); + + for (let i = startIdx; i < Math.min(endIdx, rows.length); i++) { + const row = rows[i]; + const keywordElement = row.querySelector('ess-cell keyword-text'); + const searchVolumeElement = row.querySelector('ess-cell sparkline-graph span'); + + if (keywordElement && searchVolumeElement) { + const keyword = keywordElement.textContent.trim(); + const searchVolume = cleanSearchVolume(searchVolumeElement.textContent.trim()); + + // Only add if we have both values, it's not a header, and we haven't processed this keyword yet + if (keyword && + !keyword.includes('Keyword') && + !keyword.includes('Add to plan') && + !processedKeywords.has(keyword)) { + // Add to processed set + processedKeywords.add(keyword); + // Properly escape the keyword if it contains commas + const escapedKeyword = keyword.includes(',') ? `"${keyword}"` : keyword; + csvContent += `${escapedKeyword},${searchVolume},"${source}"\r\n`; + } + } + } + } + + // Find all section headers and their positions + const rows = Array.from(document.querySelectorAll('.particle-table-row')); + const sections = []; + + // Find all sections and their boundaries + rows.forEach((row, index) => { + const cellText = row.querySelector('ess-cell')?.textContent?.trim(); + if (cellText === 'Keyword ideas' || cellText === 'Keywords you provided') { + sections.push({ + type: cellText, + start: index + 1, // Start after header + end: rows.length // Will be updated for all except last section + }); + } + }); + + // Update section end boundaries + for (let i = 0; i < sections.length - 1; i++) { + sections[i].end = sections[i + 1].start - 1; + } + + // Process each section + sections.forEach(section => { + const source = section.type === 'Keyword ideas' ? 'Keyword Ideas' : 'Provided Keywords'; + processRowsInRange(section.start, section.end, source); + }); + + if (processedKeywords.size === 0) { + alert('No keywords found in either section. Please make sure you\'re on the correct page.'); + return; + } + + // Create a temporary textarea to handle large amounts of text + const textarea = document.createElement('textarea'); + textarea.value = csvContent; + document.body.appendChild(textarea); + textarea.select(); + + try { + document.execCommand('copy'); + const notification = document.createElement('div'); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: #4CAF50; + color: white; + padding: 15px; + border-radius: 5px; + z-index: 9999; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + `; + const headerStatus = window.includeHeaders ? 'with headers ' : ''; + notification.textContent = `${processedKeywords.size} unique keywords copied to clipboard ${headerStatus}in CSV format!`; + document.body.appendChild(notification); + + setTimeout(() => notification.remove(), 3000); + } catch (err) { + alert('Failed to copy keywords: ' + err.message); + } finally { + document.body.removeChild(textarea); + } + } + + // Since this is an Angular app, content might load dynamically + // Let's try to wait for the content to load + function waitForContent() { + const maxAttempts = 20; + let attempts = 0; + + function tryFindContent() { + console.log('Attempt', attempts + 1, 'to find keyword content...'); + + // Look for the keyword grid and at least one row + const hasContent = document.querySelector('[role="grid"][aria-label="Keyword ideas"]') && + document.querySelector('.particle-table-row'); + + if (hasContent) { + console.log('Content found, proceeding with keyword extraction...'); + // Wait a bit more for all content to load + setTimeout(extractKeywords, 500); + } else if (attempts < maxAttempts) { + attempts++; + setTimeout(tryFindContent, 1000); // Wait 1 second before trying again + } else { + alert('Could not find keyword content after multiple attempts. Please make sure you are on the correct page.'); + } + } + + tryFindContent(); + } + + waitForContent(); +})(); \ No newline at end of file diff --git a/icon48.png b/icon48.png new file mode 100644 index 0000000..5414317 Binary files /dev/null and b/icon48.png differ diff --git a/icon96.png b/icon96.png new file mode 100644 index 0000000..6d262cf Binary files /dev/null and b/icon96.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..472fdda --- /dev/null +++ b/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 2, + "name": "Keyword Scraper", + "version": "1.0", + "description": "Scrapes keywords from Google Keyword Planner", + "permissions": [ + "activeTab", + "clipboardWrite", + "*://*.google.com/*" + ], + "browser_action": { + "default_popup": "popup.html" + }, + "icons": { + "48": "icon48.png", + "96": "icon96.png" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..54d9ca6 --- /dev/null +++ b/popup.html @@ -0,0 +1,91 @@ + + + + + + + + +
+
+

Would you like to include column headers in the export?

+
+ + +
+
+ + + + \ No newline at end of file diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..9b1e5bb --- /dev/null +++ b/popup.js @@ -0,0 +1,41 @@ +document.getElementById('grabKeywords').addEventListener('click', function() { + // Add class to body to expand it + document.body.classList.add('dialog-open'); + // Show the dialog and overlay + document.getElementById('dialog').style.display = 'block'; + document.getElementById('overlay').style.display = 'block'; +}); + +async function handleDialogChoice(includeHeaders) { + try { + // Hide the dialog and overlay + document.getElementById('dialog').style.display = 'none'; + document.getElementById('overlay').style.display = 'none'; + // Remove the expanded class + document.body.classList.remove('dialog-open'); + + // Get the active tab + const tabs = await browser.tabs.query({active: true, currentWindow: true}); + const activeTab = tabs[0]; + + // First inject the content script + await browser.tabs.executeScript(activeTab.id, { + code: `window.includeHeaders = ${includeHeaders};` + }); + + // Then execute the main content script + await browser.tabs.executeScript(activeTab.id, { + file: 'content-script.js' + }); + + // Close the popup after initiating the content script + window.close(); + } catch (error) { + console.error('Error:', error); + alert('Error: ' + error.message); + } +} + +// Add click handlers for the Yes and No buttons +document.getElementById('yesButton').addEventListener('click', () => handleDialogChoice(true)); +document.getElementById('noButton').addEventListener('click', () => handleDialogChoice(false)); \ No newline at end of file