Skip to content
Closed
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
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ Read more on [Getting Started](https://developer.chrome.com/extensions/getstarte
</ul>
</td>
</tr>
<tr>
<td style="vertical-align:top;">
My Bookmarks <br>
<a href="examples/bookmarks"><code>examples/bookmarks</code></a>
</td>
<td style="vertical-align:top;">
<ul>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-create">bookmarks.create</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-getTree">bookmarks.getTree</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-remove">bookmarks.remove</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-update">bookmarks.update</a></li>
</ul>
</td>
</tr>
<tr>
<td style="vertical-align:top;">
Page Redder <br>
Expand All @@ -56,15 +70,20 @@ Read more on [Getting Started](https://developer.chrome.com/extensions/getstarte
</tr>
<tr>
<td style="vertical-align:top;">
My Bookmarks <br>
<a href="examples/bookmarks"><code>examples/bookmarks</code></a>
Text Replacer <br>
<a href="examples/text-replacer"><code>examples/text-replacer</code></a>
</td>
<td style="vertical-align:top;">
<ul>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-create">bookmarks.create</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-getTree">bookmarks.getTree</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-remove">bookmarks.remove</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/bookmarks/#method-update">bookmarks.update</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/action/">action</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/commands/#event-onCommand">commands.onCommand</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/contextMenus/#method-create">contextMenus.create</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/contextMenus/#event-onClicked">contextMenus.onClicked</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/runtime/#property-lastError">runtime.lastError</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/scripting/#method-executeScript">scripting.executeScript</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/storage/#type-StorageArea">storage.get</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/storage/#type-StorageArea">storage.set</a></li>
<li><a href="https://developer.chrome.com/docs/extensions/reference/tabs/#method-query">tabs.query</a></li>
</ul>
</td>
</tr>
Expand Down
29 changes: 29 additions & 0 deletions examples/text-replacer/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
chrome.commands.onCommand.addListener((command, tab) => {
if (command == 'replace-text') {
replaceText(tab.id);
}
});

chrome.runtime.onInstalled.addListener(() => {
registerContextMenus();
});
Comment on lines +7 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During extension update, will this cause an error by trying to re-register context menu items?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would. I think I wrote it this way originally because I considered updates to be out of scope. I've updated the code as suggested in order to better set the reader up for success.

Addressed in #1718.


function registerContextMenus() {
chrome.contextMenus.create({
id: 'replace-text-menuitem',
title: 'Replace text',
});
}

chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId == 'replace-text-menuitem') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract "replace-text-menuitem" into a constant

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #1718.

replaceText(tab.id);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd normally recommend throwing an assert in here, since this is the only id we ever expect. Similarly for commands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added explicit errors for unknown commands and context menus.

Addressed in #1718.

});

function replaceText(tabId) {
chrome.scripting.executeScript({
target: {tabId},
files: ['content.js'],
});
}
47 changes: 47 additions & 0 deletions examples/text-replacer/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

// Replace text on the page using a static list of patterns
function textReplacer(replacements) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having this method is a bit confusing. I'd just move its two lines into the callback for the fetcher, and expand the comment:

// Get the patterns to replace from storage, then build a regex from them and replace all text on the page.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #1718.

const replacementPatterns = buildReplacementRegex(replacements);
replaceText(replacementPatterns);
}

function buildReplacementRegex(source) {
const output = [];
for (var i = 0; i < source.length; i++) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (var i = 0; i < source.length; i++) {
for (let i = 0; i < source.length; i++) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #1718.

if (!source[i]) { continue; }
const [find, replace] = source[i];
const sanitizedMatch = escapeRegExp(find);
const findExp = new RegExp(`\\b${sanitizedMatch}\\b`, 'gi');
output[i] = [findExp, replace];
}
return output;
}

// Use var to avoid "Identifier 'REGEXP_SPECIAL_CHARACTERS' has already been
Comment thread
dotproto marked this conversation as resolved.
// declared" errors when running multiple times on the same page.
var REGEXP_SPECIAL_CHARACTERS = /[.(){}^$*+?[\]\\]/g;
/** Sanitize user input to prevent unexpected behavior during RegExp execution */
function escapeRegExp(pattern) {
return pattern.replace(REGEXP_SPECIAL_CHARACTERS, "\\$&")
Comment thread
dotproto marked this conversation as resolved.
}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we declare node here? We only need it in the body

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Unlike for-in statements, which may contain declarations, while statements require an expression in the condition position. We also can't use for-in here because the NodeIterator interface predates JavaScript's iteration protocol. The closest we can do is move the let declaration next to the while to make their relationship more clear. I've applied this change to my new PR.

Addressed in #1718.

for (let [find, replace] of replacements) {
node.nodeValue = node.nodeValue.replace(find, replace);
}
}
}

// Replace text on the page using a list of patterns loaded from storage
chrome.storage.sync.get(['patterns'], function(data) {
textReplacer(data.patterns);
});
Comment on lines +45 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to be gradually encouraging developers to avoid storage access by content scripts unless crucial. WDYT about changing this to a message to the background page? (I don't feel strongly)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the example to route all read/writes through the background context.

Addressed in #1718.

Empty file.
Binary file added examples/text-replacer/icons/Icon24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/text-replacer/icons/icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/text-replacer/icons/icon16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/text-replacer/icons/icon19.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/text-replacer/icons/icon32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/text-replacer/icons/icon48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/text-replacer/icons/icon64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions examples/text-replacer/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "Text Replacer",
"version": "1.0.0",
"manifest_version": 3,
"description": "Replace any a line of text on a page with another line of text.",
"icons": {
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"64": "icons/icon64.png"
},
"action": {
"default_title": "Show an alert",
Comment thread
dotproto marked this conversation as resolved.
"default_icon": {
"16": "icons/icon16.png",
"24": "icons/icon24.png",
"32": "icons/icon32.png"
},
"default_popup": "popup.html"
},
"permissions": [
"scripting",
"activeTab",
"storage",
"commands",
"contextMenus"
],
"background": {
"service_worker": "background.js"
},
"commands": {
"replace-text": {
"description": "Replace text on the current page."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need a default keybinding?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, no, a default keybinding is not required. The user could still manually assign a key binding if they wanted to use it.

For the sake of clarity, I've updated the demo to have a default keyboard shortcut.

Addressed in #1718.

}
}
}
91 changes: 91 additions & 0 deletions examples/text-replacer/popup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright 2021 Google LLC

Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file or at
https://developers.google.com/open-source/licenses/bsd
*/

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%);
}
80 changes: 80 additions & 0 deletions examples/text-replacer/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<!--
Copyright 2021 Google LLC

Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file or at
https://developers.google.com/open-source/licenses/bsd
-->
<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="clear" type="button" value="Clear">
<input id="submit" type="submit" value="Replace">
</div>
</form>

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use type="module"? Then you can use top-level await in the code, rather than the awkward loading and call to loadFormData.

</body>

</html>
Loading