How To Build Your Own Safari Extensions
by Matt Swain from Mac OS X Tips
Extensions are a great way to add your own features to the Safari web browser. You can add extra buttons, toolbars and menu items, and inject scripts and style sheets into pages to modify content. To get an idea of the kinds of things that are possible, just check out the Safari Extensions Gallery. Unfortunately there's no Science section in the official gallery, but I've created a few chemistry related extensions.
This tutorial will walk you through all the steps needed to build a basic Safari Extension. Having some background in JavaScript will be useful, but building an extension is a quick and simple project so you don't need to be an expert. The end goal will be an extension that allows you to select some text on a web page and quickly search for that text in the PubChem database.
Getting Started
The first step is to sign up as a Safari developer with Apple. This is completely free, but getting your developer certificate can be an annoyingly complex process. Start by going to the Safari Developer page, click 'join now' then follow the instructions to set up your account. Once this is done, the online Safari Extension Certificate Utility will guide you through creating a certificate to make sure all the extensions you make are properly signed.
Extension Builder
Extension development is centred around the Extension Builder, which is part of Safari. This is normally hidden from view, so the first thing we have to do is enable it. Open up Safari, go to the Preferences (in the Safari menu) and in the Advanced section check the checkbox "Show Develop menu in menu bar". The Develop menu will appear, and in it you'll find the option "Show Extension Builder".
Click the plus (+) button in the bottom left of the Extension Builder window and choose "New Extension", then give it a name like "PubChemSearch" and save it anywhere you want.
Your extension will now appear in the list on the left, and the main window will contain a whole load of options for setting it up. One thing to check is that under the extension name should be a little "tick" icon and the words "Safari Developer" followed by your developer ID and email address. If this isn't the case, you haven't installed your developer certificate properly and you will need to go back to Safari Extension Certificate Utility and have another go.
The Extension Info should be fairly straightforward to fill out, along with the Bundle Identifier. Leave the Update Manifest blank for now, and don't change any of the version information. Set the Access Level to All, and tick the "Include Secure Pages" checkbox. This means the extension can be used on any web page, and won't only be available on specific sites.
To test everything works, try clicking the Install button at the top. Now, if you go to the Safari Preferences and look in the Extensions section you should find your newly created extension in the list. Unfortunately it doesn't actually do anything yet, and to go any further we are going to have to write a bit of code.
The extension folder Switch over to the Finder, and locate your extension in the place you saved it earlier. At the moment it's just a folder called "PubChemSearch.safariextension" containing a single file called "Info.plist". We are going to create two new files:
- global.html - contains all the extension logic and code that interacts with buttons, toolbars and menus
- injected.js - a script that is injected into the web page that can read, modify, add or delete content.
Just use your favourite text editor for this. TextEdit is fine (make sure it is in plain text mode by pressing Command-Shift-T) but I highly recommend the free TextWrangler. Leave the injected.js file blank for now, but add the following code the to global.html file:
<!DOCTYPE HTML> <script> safari.application.addEventListener("command", performCommand, false); function performCommand(event) { if (event.command !== "DoPubChemSearch") return; var rUrl = "http://www.ncbi.nlm.nih.gov/sites/entrez?db=pccompound&term=diazepam"; safari.application.activeBrowserWindow.activeTab.url = rUrl; } </script>
So what does this code do? The first line just says this is a HTML file, and the second and last line are script tags for embedding javascript inside a HTML file. The third line creates an "eventListener" that listens for the "command" event and calls the "performCommand" function when it detects a "command" event.
Below this we have defined the performCommand function. The first thing this function does is check what the command is. If it isn't "DoPubChemSearch" then it just returns (stops and does nothing more). If the command is "DoPubChemSearch" then it proceeds to create a variable called rUrl that contains a web address, and sets the url of the active tab to this address. Put simply, it just takes you to this page.
Linking things together Now all we need to do is link these files to the extension, and create some kind of button or menu item that calls the command "DoPubChemSearch" that our global page is listening for.
To do this, switch back to Extension Builder and change the "Global Page File" dropdown menu to "global.html" and set "injected.js" to be a start script by clicking "New Script" and choosing it from the dropdown menu.
We could use either a toolbar button or a context menu item for our extension, but here we'll use a context menu item as it'll make more sense later when we add the ability to search for the selected text. Click "New Context Menu Item" and give it a title "Search in PubChem", an identifier "ContextMenuItem" and a command "DoPubChemSearch".
Finally, click the "Reload" button at the top of the Extension Builder window (or "Install" if you haven't clicked it yet) to update the installed copy of your extension. Open up a web page, and then right-click anywhere to bring up the context menu. "Search in PubChem" should be in the menu, and clicking it should take you to the address that was specified by rUrl.
Getting the selected text At the moment, the extension just searches PubChem for diazepam, no matter what text is selected. For the extension to actually be useful, we want it to grab the currently selected text and search for that in PubChem. The injected.js file contains all the code that has access to the contents of web pages, so this is where we will put our code for grabbing the current selection. Open up injected.js, and add the following code:
document.addEventListener("contextmenu", handleMessage, false); function handleMessage(msgEvent) { var sel = ''; sel = window.parent.getSelection()+''; sel = sel.replace(/^\s+|\s+$/g,""); safari.self.tab.setContextMenuEventUserInfo(msgEvent, sel); }
The first line listens for the event "contextmenu", which is called every time the user right-clicks on the web page. When this happens, it calls the function "handleMessage". Below this we have defined the handleMessage function. It creates a blank variable called sel, and then put the contents of the current selection in it. The third line then removes any spaces from the start and the end of the selection, and then finally we set the ContextMenuEventUserInfo to sel. Setting the userInfo to sel is just a way to give our global page access to the selection. To make use of this open up global.html and change the performCommand function to the following:
function performCommand(event) { if (event.command !== "DoPubChemSearch") return; var query = event.userInfo; query = encodeURIComponent(query); query = query.replace(/%20/g,"+"); var rUrl = "http://www.ncbi.nlm.nih.gov/sites/entrez?db=pccompound&term="+query; safari.application.activeBrowserWindow.activeTab.url = rUrl; }
When the context menu item is clicked, this code now get the current selection from the userInfo. encodeURIComponent just makes sure any special characters are made suitable for a url, and the next line puts pluses between all the words. Finally, instead of rUrl just being a fixed address, the selected text is passed as the search query. As before, go to Extension Builder and click the "Reload" button before testing to see if it works. Note you will also have to reload any web pages that are already open before the extension will work with them.
If everything works, you have completed your first Safari Extension! It's pretty basic at the moment, so I've included some examples below of how to extend it, including adding some settings and an icon.
Disable Context Menu Item if nothing is selected In global.html, add the following after the performCommand function, and before the final script tag:
safari.application.addEventListener("validate", validateCommand, false); function validateCommand(event) { if (event.command !== "DoPubChemSearch") return; var query = event.userInfo; if (query.length == 0 || !query) { event.target.disabled = true; } }
The validate event is actually called pretty often when all sorts of things happen, but the one that's important to us is when the context menu is opened. All this code does is check whether the query is length zero or undefined, and if so it hides the menu item.
Display search term in context menu
Add the following code to the end of the validateCommand function:
if (query.length > 25) { query = query.substr(0,25); query = query.replace(/^\s+|\s+$/g,""); query = query + '...' } event.target.title = 'Search for "'+query+'" on PubChem';
This truncates the query if it is too long (over 25 characters), then changes the context menu item to display "Search for "'+query+'" on PubChem".
Add a setting to choose how the search results open Setting items are added using Extension Builder. Scroll down to the bottom and click "New Setting Item" and set it up as follows:
Type: Radio Buttons Title: Open results in Key: resultsType Default Value: foreground
Titles: Foreground tab, Background tab, New window, Current window Values: foreground, background, new, current
Now go to global.html and replace the last line of the performCommand function with the following:
var resultsType = safari.extension.settings.getItem("resultsType"); if(resultsType == "foreground") { var tab = safari.application.activeBrowserWindow.openTab("foreground"); tab.url = rUrl; } if(resultsType == "background") { var tab = safari.application.activeBrowserWindow.openTab("background"); tab.url = rUrl; } if(resultsType == "new") { safari.application.openBrowserWindow(); safari.application.activeBrowserWindow.activeTab.url = rUrl; } if(resultsType == "current") { safari.application.activeBrowserWindow.activeTab.url = rUrl; }
Now, before loading the search results, the performCommand function checks the settings to see how the user wants the results to open, and then acts accordingly. To test this, reload your extension in Extension Builder as usual and then go to the Safari Preferences and click on Extensions. Find your extension in the list and click on it to see the available settings.
Adding an icon Adding an icon is as simple as putting an image file in the extension folder that contains your global.html and injected.js files. In theory you need to include three images: icon-32.png, icon-48.png and icon-100.png which are 32x32px, 48x48px and 100x100px images respectively. However, in reality you can just include one and Safari will scale it up and down to use in different places. If you want, you can use the same image that I used.
Debugging
It can be pretty frustrating when things don't work properly. To get some idea of what has gone wrong you can check the console messages, but annoyingly these are in different pages for the global page and for injected scripts. For the global page, click the "Inspect Global Page" button in the Extension Builder. In the window that opens, click the "Show Console" button, which is second from left on the bottom bar. For injected scripts, choose "Start Debugging JavaScript" from the Develop menu, and click the "Show Console" button.
As well as looking at error messages, the console can be useful for logging your own messages. If you put something like
console.log("Everything so far has worked"); console.log(rUrl);
in your code, the messages and the contents of the variables will be outputted to the console, which can be really useful for diagnosing problems.
Take a peek at other people's code The easiest way to learn is to look at other peoples code and see how they do things, and luckily with Safari Extension you can peek inside any extension to see the code. This involves using the Terminal, and there's a detailed explanation here or you can use this applescript. You can try this out on any of the extensions I have made, and feel free to use any of my code in your own extensions.