Editorial
1 September 2010
Getting Started With Safari Extension Development
In this article we'll be exploring a new feature that arrived in Safari 5 - extensions. We'll go through the basics of what you need to know to start getting your hands dirty and then we'll follow on to developing a small demo extension.
Note: This article assumes that you have a Mac, Safari 5+ and a basic grasp of HTML, CSS and JavaScript.
Plug-ins vs Extensions
First things first, some of you may be wondering what the difference is between plug-ins and extensions in Safari (If you're not, you can safely skip this part).
- Plug-ins are little applications that add support for media types that aren't native to Safari e.g. Flash Player, QuickTime, Silverlight etc.
- Extensions on the other hand give us the ability to add extra features to Safari like toolbar buttons, contextual menus, extension bars and more.
So what can we do with an extension?
- Add our own Extension Bars
- Add Toolbar Buttons to Safari's toolbar
- Add Contextual Menu Items
- Inject scripts into pages
- Inject styles into pages
- Manipulate windows and tabs via the Windows and Tabs API
On top of all this we also have access to most of the latest web technologies thanks to the wonderful WebKit browser engine used by Safari. With this we get:
- HTML5 - Local data storage (not strictly part of the HTML5 spec anymore), Geolocation, Canvas, Audio/Video, WebSockets etc. Check out HTML5 Readiness for a sexy visual guide to see what is supported in Safari - i.e. pretty much everything!
- CSS3 - Safari has great (possibly the best?) CSS3 support
- Webkit's CSS extensions - 2D/3D transformation and animation, Columns, Gradients, Box sizing, Border Radius etc.
Extensions Bars

Extensions Bars give you a 30px high area in the browser window which you can fill with pretty much anything you want, things like... buttons/controls, links, information etc. Extension Bars are useful if you have a lot of information to display or if your extension requires a lot of controls.
Toolbar Buttons

Toolbar Buttons allow you to add your own buttons to Safari's toolbar. You can add as many as you like but if you're adding more than a couple Apple suggest you look at using an extension bar instead.
Contextual Menu Items

In case you don't know - contextual menus are the little menus that pop up when you right-click on things. Safari allows us to add our own items to the contextual menus that pop up over web content.
Injected scripts and styles
Safari allows us to inject both JavaScript files and CSS stylesheets into any site we wish. Injected scripts have full access to the DOM of the page they've been injected into and the same privileges as any scripts already included in the site.
Window and Tab Manipulation
Finally, we've got a pretty good API for interacting with windows and tabs to play with. With it we can do things like opening windows/tabs, closing windows/tabs, inserting tabs, getting tab titles and URLs - we can even get images of a tab's visible contents.
The Development Environment
Before you can start developing and distributing Safari extensions you're going to obtain a free developer certificate from Apple. To do this you need to register for the Safari Developer Program. Apple have made the process pretty easy so just follow the on-screen instructions to generate, download and install the certificate on your Mac.
Extension Builder

Safari provides a built-in tool called Extension Builder which you'll use to build, install, reload and uninstall your extensions. This where you'll manage all of the configuration for your Extension. I would recommend that you read Apple's Safari extensions development guide on Using Extension Builder to get all the details on the different configuration options available to you.
How to Enable Extension Builder

To enable Extension Builder you first need to make sure you have enabled the 'Show Develop menu in menu bar' option under the Advanced tab in Safari's preferences. Once that's done you'll see a 'Develop' menu at the top of the screen. This is the menu from which you can launch Extension Builder.
Demo Extension: RSS Feed Ticker

Now that you know the basics (you did pay attention didn't you?!) lets whip up a little extension to fetch, parse and display RSS feeds in a news ticker style. In this demo we'll make use of Extension Bars, the Windows and Tabs API and Settings.
Ready? Are you sure..? OK, let's get started!
Building the base for our extension
If you haven't done it already, get your Safari Developer Program certificate installed (see above) because you're going to need it now.
Open up Extension Builder from the Develop menu, click the plus (+) icon in the bottom left hand corner of the window and choose 'New Extension...'. Name your extension 'RSS Feed Ticker' and save it somewhere where you'll be able to find it again. You should now be able to see the settings for your extension with all of the required fields pre-filled for you (nice, eh!).
If you look inside the folder that Extension Builder created for our extension right now you'll see that there's only one file in there at the moment - Info.plist - which is where all the configuration options displayed in the Extension Builder GUI are stored. Don't worry about having to ever manually edit Info.plist (unless you know what you are doing) as it's all handled by Extension Builder.

So, now it's time to start adding our own files to this folder. Every Safari Extension requires at least one resource file - A HTML, JavaScript or CSS file. For our extension we're going to need the following files:
- global.html
-
This file is going to be set as our Extension Global Page in Extension Builder and it's where we'll be putting the code to handle most of the logic in our extension.
- bar.html
-
This file is going to be set as an Extension Bar in Extension Builder. It will contain code to do things like rendering the RSS feed.
- Tip: Global Page vs Extension Bar pages
-
Any code that doesn't need to be executed on a per-window basis should be in your Global Page where it will only be executed once per-session. The purpose of a Global Page is to hold logic that that doesn't require a user interface.
Open up your extensions folder in your favourite text editor (TextMate is great) and add the files we just talked about to your extension's folder and leave the files empty for now. We'll also take this opportunity to add in the jQuery JavaScript library (my personal favourite) and TexoTela's nice news ticker jQuery plugin to save me cluttering this article with too much code. Our extension folder should now look something like this:

Open up global.html and bar.html and create a basic HTML structure (I'm going to use HTML5, why not?). Don't forget to include the jQuery library into the pages. Your global.html and bar.html files should now look something like this:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="jquery-1.4.2.min.js" type="text/javascript" charset="utf-8"></script> </head> <body> </body> </html>
We also need to make sure we include TexoTela's news ticker jQuery plugin in our bar.html file so add the following into the <head> element:
<script src="jquery.newsticker.js" type="text/javascript" charset="utf-8"></script>
- Tip: URLs
-
Use relative URLs when referencing resources within your extension unless you are using an injected script, in which case you'll need to use absolute URLs. You can read more about how to use absolute URLs within injected scripts in the docs.
Implementing the basic functionality
Before we write any more code let's take a minute to think about the functionality which we'll need from our plugin. Here's what we'll need it to do:
Initialisation
- Initiate the retrieval of the feed (see Feed Retrieval below)
- Automate the retrieval of the feed every so often to keep up to date with the feed
Feed Retrieval
- Make an AJAX request to retrieve the feed as XML
- Parse the returned XML
- Tell all instances of our extension bar to render the parsed feed
Feed Rendering
- Grab the data we got from parsing the feed's XML
- Render the data out to the DOM as HTML elements
That's good enough for now, we'll add some more features later on. The initialisation and feed retrieval logic can go into our Global Page because we only need that code to run once per session, but the feed rendering logic would be better placed in our Extension Bar page because it's going to need to interact with the DOM to display the feed.
Let's flesh out some JavaScript functions to handle the functionality we have defined:
global.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="jquery-1.4.2.min.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
// We'll be storing the parsed feed items in the following array
var feedItems = new Array();
function FeedItem() {};
FeedItem.prototype = {
link: null,
title: null
};
// Run the initial feed retrieval and set a timer to
// execute the feed retrieval every 10 minutes
function initialise() {
retrieveFeed();
setTimeout(retrieveFeed, 100000);
}
function retrieveFeed() {
$.ajax({
type: "GET",
url: "http://news.google.co.uk/news?output=rss",
dataType: "xml",
success: parseFeed,
error: function(XMLHttpRequest, textStatus, errorThrown) {
// In the real world we'd handle errors properly
}
});
}
function parseFeed(feed) {
// Clear out the feedItems array
feedItems = new Array();
$(feed).find("item").each(function() {
var item = new FeedItem(), $this = $(this);
item.link = $this.find("link:first").text();
item.title = $this.find("title:first").text();
feedItems.push(item);
});
renderExtensionBars();
}
function renderExtensionBars() {
var bars = safari.extension.bars;
$.each(bars, function(i) {
bars[i].contentWindow.render();
});
}
$(document).ready(initialise);
</script>
</head>
<body>
</body>
</html>
And we'll add the JavaScript, HTML and CSS for the extension bar:
bar.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="jquery-1.4.2.min.js" type="text/javascript" charset="utf-8"></script>
<script src="jquery.newsticker.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
// We'll use this to gain access to functions and variables
// in the Global Page
const global = safari.extension.globalPage.contentWindow;
function render() {
// Create an unordered list of the feed items
// using the feedItems Array from our Global Page
// and add it all to the DOM
var $container = $("#items");
// Clear the container's contents
$container.empty();
$.each(global.feedItems, function() {
var item = this;
var $item = $("<li>");
$("<h1>", {
text: item.title
}).click(function() {
// Assign a click event that will open the feed
// item's URL in a new tab (or the current one if
// it's empty)
openLink(item.link);
return false;
}).appendTo($item);
$item.appendTo($container);
// We're using TexoTela's nice news ticker jQuery plugin
// to fade between the feed items with some nice animation
$container.newsTicker(8000);
});
}
// A little bit of code that utilises the Windows and Tabs API
// to decide whether or not to open the link in a new tab
//
// Windows and Tabs API Docs: http://bit.ly/bineMD
function openLink(url) {
if (safari.self.browserWindow.activeTab.url != "") {
safari.self.browserWindow.openTab().url = url;
} else {
safari.self.browserWindow.activeTab.url = url;
}
}
$(document).ready(render);
</script>
<style type="text/css">
* {
margin: 0; padding: 0;
font-size: 11px;
}
body {
margin: 0; padding: 0 10px;
-webkit-font-smoothing: antialias;
text-align: center;
}
ul, ul li, ul li h1 {
width: 100%; height: 30px;
display: block;
}
ul li h1 {
overflow: hidden;
cursor: pointer;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 28px;
}
ul li h1:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<ul id="items"></ul>
</body>
</html>
- Tip: Function and variable names
-
You don't need to worry about function and variable name conflicts between your extension and any scripts in a web page because extensions exist inside their own little environments.
- Tip: Cross-domain AJAX requests
-
In everyday web development we're not allowed to make cross-domain AJAX requests for security purposes unless you're able to use callbacks, but we don't need to worry about that problem within our extension - you only need to make sure that your extension has the correct permissions set for Extension Website Access in Extension Builder.
Before we can test it out we need to go back into Extension Builder and tell it about our pages.
Firstly, set global.html as your Global Page using the pull-down menu. Then click the 'New Bar' button to add a bar with the following settings:
- Label: RSS Feed Ticker (this is used in the context of 'Show/Hide [label] Bar')
- File: bar.html
- Identifier: 'rssFeedTicker' as the identifier.
Important: We need to remember to set the Extension Website Access Access Level option to 'All' otherwise nothing will work and you'll probably end up smashing your head into the desk in frustration.
If you've set up everything correctly you should now be able to click 'Install' at the top of the Extension Builder window and see your extension bar get added to any Safari windows you have open, woo! Pretty easy wasn't it?
Adding a few more features
You may have noticed that we hardcoded the RSS feed URL into our extension and that probably isn't going be that useful in the real world, so wouldn't it be nice if we could let the user choose the feed source themselves?
Allowing the user to choose their own RSS feed
Fortunately, Safari allows us to store settings for our Extensions and provides an API for accessing them programmatically. User editable settings will appear in your extension's preference pane inside Safari's preferences dialog.
In Extension Builder, scroll down to Extension Settings and click the 'New Setting Item' button. For our item we want the following settings:
- Type: Text Field
- Title: Feed URL
- Key: feedUrl
- Default Value: http://news.google.co.uk/news?output=rss (We'll use Google News as the default feed for our Extension)

You may have noticed the little checkbox about storing this setting item securely. If you're going to be storing sensitive information like email addresses, passwords, logins then you're going to need to use this option - for our extension we won't be needing it.
Note: There will now be a new file in your extension's folder called Settings.plist which is where your settings configuration is stored.
Finally, we'll need to update the code in our Global Page (global.html) to make use our new setting item, edit the url parameter of the AJAX call in the retrieveFeed() function:
function retrieveFeed() {
$.ajax({
type: "GET",
url: safari.extension.settings.feedUrl,
dataType: "xml",
success: parseFeed,
error: function(XMLHttpRequest, textStatus, errorThrown) {
// In the real world we'd handle errors properly
}
});
}
If you want to find out more about accessing your extension's settings then make sure you take a look at the docs for all the details.
If we were to leave it there then the feed source wouldn't actually change until the next time the retrieveFeed() function gets called by our timer. Instead, lets add some code to call the retrieveFeed() function whenever the feed URL is changed in our settings.
Whenever a setting is changed (either pragmatically or by a user) Safari omits a "change" event which we can listen for. Here's the code we'll add to our Global Page (global.html):
function settingsChanged(event) {
if (event.key == "feedUrl") {
retrieveFeed();
}
}
// Tell Safari that we want to listen for the "change" event
safari.extension.settings.addEventListener("change", settingsChanged, false);
Our extension should now be fully functioning. Now the user can go into our extensions preference pane and change the feed URL any time they feel like it - time to reload and admire our work!
We've made it to the finishing line

And we're done! Hopefully you've enjoyed following along with our example extension. With any luck you'll have learnt enough to get you started on your own extensions (Drop me a message on Twitter (@alistairholt) if you create anything cool!). And finally, thank you for reading!