DEV Community

Cover image for How to Rapidly Build Browser Extensions With AI
Boone Cabal
Boone Cabal

Posted on

How to Rapidly Build Browser Extensions With AI

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  
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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" }
}
Enter fullscreen mode Exit fullscreen mode

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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();
})();
Enter fullscreen mode Exit fullscreen mode

Step 2 — Installing and Testing Your Extension

Follow these steps to test your extension in Chrome:

  1. Open Chrome Extensions Page
    Navigate to chrome://extensions.

  2. Enable Developer Mode
    Toggle Developer Mode on (top right).

  3. Load Unpacked Extension
    Click Load Unpacked and select your HighlighterExtension folder.

  4. 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.  
Enter fullscreen mode Exit fullscreen mode

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();
})();
Enter fullscreen mode Exit fullscreen mode

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:

    1. Selects text on a webpage.
    2. Clicks the custom browser-action icon.
    3. 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:
    4. Action 1 (Highlight): If the currently selected text is not highlighted, apply a highlight to that specific text.
    5. 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();
})();
Enter fullscreen mode Exit fullscreen mode

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:

  1. Revert files to the last Git commit.
  2. Start a new chat session with an AI assistant.
  3. 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)