Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions functional-samples/sample.text-replacer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Text Replacer

This sample demonstrates how an extension can replace text on a page in response to user invocation.

## Overview

Users can use the extension's popup to define up to 5 pairs of text replacements. Text is replaced when the user clicks the "Replace" button in the popup, when they select the "Replace text" context menu option on a page, or when they use the extension's keyboard shortcut (command).

## Compatibility

`"minimum_chrome_version"` is set to 148 because that version adds support for returning a promise from an `onMessage` event handler ([docs](https://developer.chrome.com/docs/extensions/develop/concepts/messaging#:~:text=call%20it%20later.-,Return%20a%20promise,-From%20Chrome%20148)) to respond to a message asynchronously.

## Running this extension

1. Clone this repository.
2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked).
3. Navigate to any page (make sure that the URL doesn't start with `chrome://`).
4. Click the extension icon.
70 changes: 70 additions & 0 deletions functional-samples/sample.text-replacer/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

const REPLACE_TEXT_MENUITEM_ID = 'replace-text-menuitem';
const REPLACE_TEXT_COMMAND_ID = 'replace-text-command';
const GET_REPLACEMENTS_MESSAGE_ID = 'get-replacements';
const SET_REPLACEMENTS_MESSAGE_ID = 'set-replacements';

chrome.runtime.onInstalled.addListener(async () => {
// Remove any previously registered context menus to avoid conflicts
await chrome.contextMenus.removeAll();

// Register the new set of context menus
await chrome.contextMenus.create({
id: REPLACE_TEXT_MENUITEM_ID,
title: 'Replace text'
});
});

chrome.commands.onCommand.addListener((command, tab) => {
if (command == REPLACE_TEXT_COMMAND_ID) {
replaceText(tab.id);
} else {
throw new Error(`Unknown command executed with ID "${command}"`);
}
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId == REPLACE_TEXT_MENUITEM_ID) {
replaceText(tab.id);
} else {
throw new Error(
`Unknown context menu option clicked with ID "${info.menuItemId}"`
);
}
});

// Route all storage reads/writes through the background so we have a single
// source of truth
chrome.runtime.onMessage.addListener((message) => {
switch (message.id) {
case GET_REPLACEMENTS_MESSAGE_ID:
// Fall back to an empty array 'patterns' is not set
return chrome.storage.sync.get({ patterns: [] });

case SET_REPLACEMENTS_MESSAGE_ID:
return chrome.storage.sync.set({ patterns: message.data });

default:
throw new Error(`Unknown message received with ID "${message.id}"`);
}
});

function replaceText(tabId) {
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
}
65 changes: 65 additions & 0 deletions functional-samples/sample.text-replacer/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Wrap the in an immediately invoked function expression (IIFE) in order to
// prevent local variables from polluting global scope
(function () {
const GET_REPLACEMENTS_MESSAGE_ID = 'get-replacements';

function buildReplacementRegex(source = []) {
const output = source
.filter(([find] = []) => find)
.map(([find, replace]) => {
const sanitizedMatch = escapeRegExp(find);
const findExp = new RegExp(`\\b${sanitizedMatch}\\b`, 'gi');
return [findExp, replace];
});
return output;
}

// For example purposes only. This may not cover all special characters used
// in regular repressions.
const REGEXP_SPECIAL_CHARACTERS = /[.(){}^$*+?[\]\\]/g;

/** Sanitize user input to prevent unexpected behavior during RegExp execution */
function escapeRegExp(pattern) {
return pattern.replace(REGEXP_SPECIAL_CHARACTERS, '\\$&');
}

/** Iterate through all text nodes and replace */
function replaceText(replacements) {
const nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_TEXT
);

let node;
while ((node = nodeIterator.nextNode())) {
for (let [find, replace] of replacements) {
// Guard against nullish replacement values
replace ??= '';
node.nodeValue = node.nodeValue.replace(find, replace);
}
}
}

// Get the patterns to replace from storage, then build a regex from them and
// replace all text on the page.
chrome.runtime
.sendMessage({ id: GET_REPLACEMENTS_MESSAGE_ID })
.then((data) => {
const replacementPatterns = buildReplacementRegex(data.patterns);
replaceText(replacementPatterns);
});
})();
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions functional-samples/sample.text-replacer/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "Text Replacer",
"version": "1.0.0",
"manifest_version": 3,
"description": "Replace any line of text on a page with another line of text.",
"minimum_chrome_version": "148",
"icons": {
"16": "icons/icon16.png",
"24": "icons/icon24.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"64": "icons/icon64.png",
"128": "icons/icon128.png"
},
"action": {
"default_title": "Manage text replacements",
"default_icon": {
"16": "icons/icon16.png",
"24": "icons/icon24.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"64": "icons/icon64.png"
},
"default_popup": "popup.html"
},
"permissions": [
"scripting",
"activeTab",
"storage",
"commands",
"contextMenus"
],
"background": {
"service_worker": "background.js"
},
"commands": {
"replace-text-command": {
"description": "Replace text on the current page.",
"suggested_key": {
"default": "Alt+R",
"mac": "MacCtrl+R"
}
}
}
}
99 changes: 99 additions & 0 deletions functional-samples/sample.text-replacer/popup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

html, body {
height: 200px;
width: 400px;
margin: 0;
padding: 0;
font-size: 16px;
}

body {
padding-bottom: 2em;
}

.form--table {
width: 100%;
border-collapse: collapse;
}

.form--input {
width: 100%;
padding: .25rem .5rem;
background: #eee;
box-sizing: border-box;
border: 1px solid #0004;
line-height: 1.5;
}

th {
text-transform: uppercase;
font-size: .7rem;
color: hsl(0, 0%, 30%);
}

td {
padding: .25rem .1rem;
}

td:first-child {
padding-left: .25rem;
}
td:last-child {
padding-right: .25rem;
}

.form--controls {
position: fixed;
display: flex;
bottom: 0;
right: 0;
left: 0;
}
.form--controls > * {
flex: 1;
}

.form--controls > * {
border: none;
height: 2rem;
margin: .1rem;
cursor: pointer;
border: 1px solid hsla(0, 0%, 0%, .2);
}

.form--controls > *:first-child {
margin-left: .25rem;
}
.form--controls > *:last-child {
margin-right: .25rem;
}

#clear {
background: hsl(0, 0%, 100%);
}
#clear:hover,
#clear:focus {
background: hsl(0, 0%, 80%);
}
#submit {
background: hsl(190, 100%, 60%);
}
#submit:hover,
#submit:focus {
background: hsl(190, 80%, 50%);
}
88 changes: 88 additions & 0 deletions functional-samples/sample.text-replacer/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<!--
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>Document</title>
<link rel="stylesheet" href="popup.css">
</head>

<body>
<form>
<table class="form--table">
<thead>
<tr>
<th>Find</th>
<th>Replace</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="text" class="form--input">
</td>
<td>
<input type="text" class="form--input">
</td>
</tr>
<tr>
<td>
<input type="text" class="form--input">
</td>
<td>
<input type="text" class="form--input">
</td>
</tr>
<tr>
<td>
<input type="text" class="form--input">
</td>
<td>
<input type="text" class="form--input">
</td>
</tr>
<tr>
<td>
<input type="text" class="form--input">
</td>
<td>
<input type="text" class="form--input">
</td>
</tr>
<tr>
<td>
<input type="text" class="form--input">
</td>
<td>
<input type="text" class="form--input">
</td>
</tr>
</tbody>
</table>

<div class="form--controls">
<input id="reset" type="reset" value="Clear" />
<input id="submit" type="submit" value="Replace" />
</div>
</form>

<script src="popup.js"></script>
</body>

</html>
Loading