Introduction
In this tutorial, let's create a browser extension that highlights text on a webpage using AI code generation technologies. We’ll leverage the Google Gemini Coding Partner gem and DeepSeek to streamline code generation for our browser extension. When you are done reading this article, you will understand how massive a paradigm shift AI code generators are.
Step 1 — Generating Project Code Using README.MD
Much of our code generation revolves around creating and updating a README.MD
file, serving as a dynamic features document that guides your extension’s development.
Let's begin by executing the following README.MD
features in Gemini:
# Browser Extension - Simple Page Text Highlighter
## Features
- Custom bullseye Browser-Action icon
- Highlights any text you have selected on the webpage
Running this as a prompt generated a lot of output. We are only going to focus on a couple of excerpts--the first recommends organizing your project with the structure:
HighlighterExtension/
├── manifest.json
├── background.js
├── content.js
└── icons/
├── icon16.png
├── icon64.png
└── icon128.png
For the Browser-Action extension icon, I downloaded PNG files from Flaticon.
Additionally, Gemini generated the core extension files; copy the code over to your project.
manifest.json
{
"manifest_version": 3,
"name": "Simple Page Text Highlighter",
"version": "1.0",
"description": "Highlights any text you have selected on the webpage.",
"permissions": ["activeTab", "scripting"],
"background": { "service_worker": "background.js" },
"action": {
"default_icon": "icons/icon128.png",
"default_title": "Highlight Selected Text"
},
"icons": { "128": "icons/icon128.png" }
}
background.js
chrome.action.onClicked.addListener((tab) => {
// Execute the content.js script on the current active tab
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
})
.then(() => {
console.log('Content script injected successfully!');
})
.catch(err => {
console.error('Failed to inject content script:', err);
});
});
content.js
(function() {
function highlightSelectedText() {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.style.backgroundColor = 'yellow'; // You can change this color
span.style.fontWeight = 'bold'; // Add some styling, if you like
// Wrap the selected content with the span
range.surroundContents(span);
// Clear the selection after highlighting
selection.removeAllRanges();
} else {
alert('Please select some text to highlight!');
}
}
// Execute the highlight function
highlightSelectedText();
})();
Step 2 — Installing and Testing Your Extension
Follow these steps to test your extension in Chrome:
Open Chrome Extensions Page
Navigate tochrome://extensions
.Enable Developer Mode
Toggle Developer Mode on (top right).Load Unpacked Extension
Click Load Unpacked and select yourHighlighterExtension
folder.-
Test It Out
- Open any webpage (e.g., Google).
- Select text with your mouse.
- Click the extension icon in your toolbar.
- The selected text should now be highlighted in yellow!
Step 3 — Adding a Toggle Feature
Now, let’s refine the extension by making highlighted text toggleable. When I was building this extension, Gemini failed to implement this feature, so I switched to DeepSeek, which successfully generated a functional solution.
First, I updated my README.MD
, adding the toggling feature just described:
Updated README.MD:
# Browser Extension - Simple Page Text Highlighter
## Features
- Custom bullseye Browser-Action icon
- Highlights any text you have selected on the webpage
- Clicking the icon will:
1. Highlight the selected text if it isn’t already highlighted.
2. Unhighlight selected text if it’s already highlighted.
Executing this prompt in DeepSeek generated a new content.js
, which (partially) unhighlighted text:
content.js
(function() {
const HIGHLIGHT_CLASS = 'simple-page-text-highlighter-highlight';
function highlightSelectedText() {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const selectedText = selection.toString().trim();
if (!selectedText) {
alert('Please select some text to highlight or unhighlight!');
return;
}
// Check if selection is already highlighted
let isAlreadyHighlighted = false;
const parentElement = range.commonAncestorContainer;
// Function to check if node or its parents have our highlight class
const hasHighlight = (node) => {
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(HIGHLIGHT_CLASS)) {
return true;
}
if (node.parentNode) {
return hasHighlight(node.parentNode);
}
return false;
};
// Check start and end containers
isAlreadyHighlighted = hasHighlight(range.startContainer) || hasHighlight(range.endContainer);
if (isAlreadyHighlighted) {
// Unhighlight the selected text
const spans = document.querySelectorAll(`.${HIGHLIGHT_CLASS}`);
spans.forEach(span => {
// Check if this span is within our selection
const spanRange = document.createRange();
spanRange.selectNodeContents(span);
if (range.intersectsNode(span)) {
// Unwrap the span
const parent = span.parentNode;
while (span.firstChild) {
parent.insertBefore(span.firstChild, span);
}
parent.removeChild(span);
}
});
} else {
// Highlight the selected text
const span = document.createElement('span');
span.style.backgroundColor = 'yellow';
span.style.fontWeight = 'bold';
span.classList.add(HIGHLIGHT_CLASS);
try {
// First, try the simple approach
if (range.collapsed) {
return;
}
range.surroundContents(span);
} catch (e) {
// If surroundContents fails, try a more robust approach
console.warn("Could not surround contents directly. Trying alternative method:", e);
// Extract the selected text nodes
const textNodes = [];
const walker = document.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_TEXT,
{
acceptNode: node => {
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
}
);
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
// Process each text node
textNodes.forEach(textNode => {
const parent = textNode.parentNode;
if (parent.classList && parent.classList.contains(HIGHLIGHT_CLASS)) {
return; // Skip already highlighted nodes
}
const newNode = document.createElement('span');
newNode.style.backgroundColor = 'yellow';
newNode.style.fontWeight = 'bold';
newNode.classList.add(HIGHLIGHT_CLASS);
parent.replaceChild(newNode, textNode);
newNode.appendChild(textNode);
});
}
}
// Clear the selection after highlighting/unhighlighting
selection.removeAllRanges();
} else {
alert('Please select some text to highlight or unhighlight!');
}
}
highlightSelectedText();
})();
Step 4 — Fixing the Unhighlighting Bug
Our implementation fails when unhighlighting one word within multiple highlighted words. Unhighlighting one word in a highlighted range of three erroneously unhighlights all three words.
To solve this, I gave Gemini the last README.MD
prompt and had it generate a much better, more detailed revision:
README.MD
Browser Extension: Simple Page Text Highlighter
This extension will enable users to highlight and unhighlight selected text on a webpage.
Functionality Details
- Browser-Action Icon: The extension will have a custom "bullseye" icon displayed in the browser's toolbar.
Interaction Trigger: The core functionality activates when a user performs two sequential actions:
- Selects text on a webpage.
- Clicks the custom browser-action icon.
- Highlighting Logic (On Icon Click): Upon clicking the browser-action icon with text selected, the extension will evaluate the selected text and perform one of two actions:
- Action 1 (Highlight): If the currently selected text is not highlighted, apply a highlight to that specific text.
- Action 2 (Unhighlight): If the currently selected text is highlighted, remove the highlight from only that specific selected text.
Granularity of Action: The highlighting or unhighlighting action applies exclusively to the text currently selected by the user. For example, if a user selects one word within a previously highlighted phrase of three words, only the selected word will be unhighlighted, leaving the other two words in the phrase highlighted.
I find this approach to be a novel solution because instead of writing a prompt that generates code, we are writing a prompt that generates an improved prompt.
Now, executing this improved prompt in DeepSeek generates the following content.js
, which correctly unhighlights ranges of highlighted words:
content.js
(function () {
const HIGHLIGHT_CLASS = "simple-page-text-highlighter-highlight";
// Helper function to get the intersection between two ranges
function getIntersection(range1, range2) {
const start =
range1.compareBoundaryPoints(Range.START_TO_START, range2) < 0
? range2.startContainer
: range1.startContainer;
const startOffset =
range1.compareBoundaryPoints(Range.START_TO_START, range2) < 0
? range2.startOffset
: range1.startOffset;
const end =
range1.compareBoundaryPoints(Range.END_TO_END, range2) < 0
? range1.endContainer
: range2.endContainer;
const endOffset =
range1.compareBoundaryPoints(Range.END_TO_END, range2) < 0
? range1.endOffset
: range2.endOffset;
if (
range1.compareBoundaryPoints(Range.START_TO_END, range2) < 0 ||
range1.compareBoundaryPoints(Range.END_TO_START, range2) > 0
) {
return null; // No intersection
}
const intersection = document.createRange();
intersection.setStart(start, startOffset);
intersection.setEnd(end, endOffset);
return intersection;
}
function highlightSelectedText() {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const selectedText = selection.toString().trim();
if (!selectedText) {
alert("Please select some text to highlight or unhighlight!");
return;
}
// Check if selection is already highlighted
let isAlreadyHighlighted = false;
const parentElement = range.commonAncestorContainer;
const hasHighlight = (node) => {
if (
node.nodeType === Node.ELEMENT_NODE &&
node.classList.contains(HIGHLIGHT_CLASS)
) {
return true;
}
if (node.parentNode) {
return hasHighlight(node.parentNode);
}
return false;
};
isAlreadyHighlighted =
hasHighlight(range.startContainer) || hasHighlight(range.endContainer);
if (isAlreadyHighlighted) {
const spans = document.querySelectorAll(`.${HIGHLIGHT_CLASS}`);
spans.forEach((span) => {
const spanRange = document.createRange();
spanRange.selectNodeContents(span);
const intersection = getIntersection(range, spanRange);
if (!intersection) return;
const selectedContent = intersection.cloneContents();
const parent = span.parentNode;
// Split the span into three parts: before, selected, and after
const beforeRange = document.createRange();
beforeRange.setStart(span, 0);
beforeRange.setEnd(
intersection.startContainer,
intersection.startOffset
);
const beforeContent = beforeRange.cloneContents();
const afterRange = document.createRange();
afterRange.setStart(
intersection.endContainer,
intersection.endOffset
);
afterRange.setEnd(span, span.childNodes.length);
const afterContent = afterRange.cloneContents();
// Replace the original span with the split parts
const fragment = document.createDocumentFragment();
if (beforeRange.toString().trim()) {
const beforeSpan = document.createElement("span");
beforeSpan.className = HIGHLIGHT_CLASS;
beforeSpan.style.backgroundColor = "yellow";
beforeSpan.style.fontWeight = "bold";
beforeSpan.appendChild(beforeContent);
fragment.appendChild(beforeSpan);
}
fragment.appendChild(selectedContent);
if (afterRange.toString().trim()) {
const afterSpan = document.createElement("span");
afterSpan.className = HIGHLIGHT_CLASS;
afterSpan.style.backgroundColor = "yellow";
afterSpan.style.fontWeight = "bold";
afterSpan.appendChild(afterContent);
fragment.appendChild(afterSpan);
}
parent.replaceChild(fragment, span);
});
} else {
// Highlight the selected text (original code)
const span = document.createElement("span");
span.style.backgroundColor = "yellow";
span.style.fontWeight = "bold";
span.classList.add(HIGHLIGHT_CLASS);
try {
if (range.collapsed) {
return;
}
range.surroundContents(span);
} catch (e) {
console.warn(
"Could not surround contents directly. Trying alternative method:",
e
);
const textNodes = [];
const walker = document.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
return range.intersectsNode(node)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
}
);
let node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
textNodes.forEach((textNode) => {
const parent = textNode.parentNode;
if (
parent.classList &&
parent.classList.contains(HIGHLIGHT_CLASS)
) {
return;
}
const newNode = document.createElement("span");
newNode.style.backgroundColor = "yellow";
newNode.style.fontWeight = "bold";
newNode.classList.add(HIGHLIGHT_CLASS);
parent.replaceChild(newNode, textNode);
newNode.appendChild(textNode);
});
}
}
selection.removeAllRanges();
} else {
alert("Please select some text to highlight or unhighlight!");
}
}
highlightSelectedText();
})();
Go ahead and test it, fiend. Highlight two words, and then unhighlight one of them.
Bro Tips
💡 Tip 1: Write Clear AI Prompts
- Keep your
README.MD
updated, adding one feature at a time. - Use AI-generated code to implement specific features incrementally.
🛠️ Tip 2: Undo AI Code Mistakes
If AI assistance fails:
- Revert files to the last Git commit.
- Start a new chat session with an AI assistant.
- Upload project files and paste your README.MD for fresh results.
🔄 Tip 3: Use Multiple AI Assistants
If one fails, try another! DeepSeek often delivers better code than Gemini or Grok.
Conclusion
If you made it this far, congratulations—you’re highly irregular. Nevertheless, thanks for reading, friend. Now leave, please. 🚀
Top comments (0)