Save CSS - Chrome extension makes you fly

April 2012

My first Chrome extension Save CSS is now available in the Chrome web store!

If you are a web developer working with CSS or Javascript and using the great built-in Developer tools in the Chrome browser, then this extension can improve your workflow a lot. I know it has boosted my own work.

In a typical workflow of web user interface development, you write a little bit of HTML, Javascript and CSS, then view the result live in a browser where you continue tweaking the CSS with Developer tools in Chrome/Safari or with Firebug in Firefox. When all the tweaks are finished, you then copy the modified CSS values manually one by one and paste them back into your editor, save all files, and finally view the results again in a browser to verify that you remembered to copy everything. You repeat these steps a hundred times a day. Clearly there is room for improvement.

This extension speeds up the workflow by eliminating the manual copy paste part and by automatically saving your CSS and Javascript modifications done in Developer tools into the original source files in your local disk.

I have been using this extension privately since October 2011 when the experimental APIs became available in early Dev versions of Chrome. The API became officially supported this week when Chrome 18 was released and I was finally able to publish this extension in the Chrome store.

Setup

Fire up your Chrome browser and install the extension from the Chrome web store.

Then download the required Python script or Ruby script to your computer, either one, it doesn't matter since they are identical in functionality. Run the script from any folder: python server.py or ruby server.rb. The script starts a small web server that actually writes the changes sent by Chrome into the local disk since Chrome can't save files for security reasons.

Of course, first make sure you have either Python or Ruby installed on your computer. The scripts do not require any additional libraries to install.

After installation, launch Developer Tools in Chrome. The extension user interface is visible as the last tab, "Save":

Then browse to the site whose CSS or Javascript you want to modify. Most likely you run the development server on local computer and you have all the source files in the local file system.

Now change any CSS property in the Elements-tab, go to Save-tab and you will see an error that says no mapping found. The extension issues an error since it doesn't yet know where to save the changes to. You need to add a mapping rule for the site to map an URL to a local path.

Click Create and a new mapping will be added with a prefilled URL. Remove the file name part from the URL, and enter an absolute path to a local folder where the files are saved to. This path should point to the folder where you store your CSS and Javascript files of your site.

Finally click Save and the mapping gets saved.

Now change a CSS again in the Elements-tab. If everything was configured correctly, the CSS was saved into local disk and you should see an entry in the "Saved Files" table. If not, you should see an error.

Here's a screenshot with one mapping and two successful saves:

Usage

All the standard Chrome Developer Tools operations are available for CSS and Javascript tweaking. Here's the summary of what you can do:

To change a CSS property, double-click either on the property name or the value and edit the value with a keyboard. Remember that arrow up and down can change numeric values.

To add a new CSS property, double-click on the last line of a selector, the one with "}", and start typing a new property name.

To delete a CSS property, double-click on the property name and delete the whole name and press enter.

To add a new CSS selector, go to the Resources tab and select the desired CSS file (remember the shortcut link in the Elements tab too). Double-click and edit the CSS file as you wish. When you press CMD-S, the whole file is saved and CSS becomes active.

To change Javascript code, go to the Resources tab and select the desired Javascript file. Edit and save the whole file with CMD-S. Note that in Chrome, there is no need to reload the page since the V8 engine compiles Javascript just in time.

And don't be afraid of loosing your comments in the CSS or Javascript files - comments are preserved by Chrome. Even vendor prefixes are properly preserved. Chrome provides solid piece of tech.

The extension doesn't work with CSS compilation setups like LESS since the extension operates at the level of raw CSS and not at the higher level.

If you want to be careful with your CSS/JS tweakings, you can disable the automatic save functionality by unchecking the checkbox at the bottom of the "Save" tab. Then you can control when to save and what files to save. I'm not sure how useful this option is but it's there.

Under the hood

The extension relies on the following devtools API of Chrome:

// request browser to call us when CSS or JS is changed by dev tools
chrome.devtools.inspectedWindow.onResourceContentCommitted.addListener(
    function(resource, content) {
    // CSS or JS was changed, we got the content

    // content:       the body of CSS or JS file
    // resource.url:  the url of file
    // resource.type: type of file (css, javascript, document)

});

This powerful API became officially supported in Chrome 18 in March 28, 2012.

The API is very simple: whenever a CSS or Javascript file is modified in the Developer tools, this callback gets called in the extension. The whole content of the CSS or Javascript file is provided, and all the hard work of merging new changes in has been done by Chrome.

Since Chrome extensions do not have access to the local file system, the write operation needs to be done externally. The extension thus sends the content to an external script via AJAX. Being a Python developer, I wrote the script in Python. The script just receives the content and an absolute destination file path, and writes the file to disk. In case of error, it returns a message.

The external script is simple since all the configurations are taken care of by the extension. Mappings are stored permanently into localstorage which is private to the extension.

Source code

The whole source code to the extension and to the Python and Ruby web server scripts are available in GitHub.

I'm a long-time Python developer but wrote the script also in Ruby for the fun of it. Here they are both side by side, like twin brothers.

Server scripts in Python and Ruby:

#!/usr/bin/python
# -*- coding: utf-8 -*-

# server.py: receive CSS and JS files from Chrome extension
#   and save files locally
#
# Author: Tomi.Mickelsson@iki.fi
#   30.10.2011 - Created

from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler

class MyServer(BaseHTTPRequestHandler):

    def do_POST(self):
        hd = self.headers

        # chrome sent data:
        url   = hd.get("X-origurl")
        fpath = hd.get("X-filepath")
        bodylen = int(hd['content-length'])
        body    = self.rfile.read(bodylen)
        print url, " ->", fpath, len(body)

        reply = "OK"

        # save file
        try:
            f = open(fpath, "w")
            f.write(body)
            f.close()
        except Exception, e:
            print e
            reply = "Server couldn't save "+fpath

        # return reply
        self.send_response(200)
        self.end_headers()
        self.wfile.write(reply)

# start http server
server = HTTPServer(('localhost', 8080), MyServer)
print "Server running in port 8080..."
server.serve_forever()
#!/usr/bin/ruby
# -*- coding: utf-8 -*-

# server.rb: receive CSS and JS files from Chrome extension
#   and save files locally
#
# Author: Tomi.Mickelsson@iki.fi
#   04.02.2012 - Created

require 'webrick'
include WEBrick

class MyServlet < HTTPServlet::AbstractServlet

  def do_POST(req, res)
    url   = req.header['x-origurl']
    fpath = req.header['x-filepath']
    bodylen = req.header['content-length']
    raw = req.body
    fpath = fpath.join("")

    print url, " -> ", fpath, " ", bodylen, "\n"

    reply = "OK"

    # save file
    begin
        f = File.open(fpath, "wb")
        f.syswrite(raw)
        f.close
    rescue => e
        puts "EXCEP " + e.message
        reply = e.message
    end

    res.status = "200"
    res.body = reply
  end

end

# start server
server = HTTPServer.new(:BindAddress => "localhost",:Port => 8080)
server.mount('/', MyServlet)

trap 'INT' do server.shutdown end

puts "Server running in port 8080..."
server.start

Issues

If you have issues with the extension, you can report them below or at GitHub.

Update: The Python script added extra line feeds on Windows. Fixed.

Back