Blogging with TextMate, and using AppleScript and JavaScript to ease the pain

February 15, 2010

Let me begin by saying that I endeavored initially to get this working with Google Chrome (my main browser these days), but because Chrome's AppleScript support currently is very minimal, it just wasn't possible. In light of that I chose to at least proof-of-concept the idea with Safari/WebKit so that I can later use this post to inform my solution for Chrome (and so others can take advantage of this now if they happen to use Safari). (Update: I've come up with a solution for Chrome.)

A few weeks ago I decided I had to have support for Markdown syntax highlighting in my blogging client, which at the time (and for many years prior) was MarsEdit. After coming to terms with the fact that such highlighting just wasn't going to happen in MarsEdit (any time soon), I set out to find another solution, and quickly remembered that TextMate (which may be my favorite application of all time) came out with a blogging bundle many years ago. After watching the screencast, running some tests and creating a couple of blogging templates, I very quickly was using TextMate to write and publish.

Doing all of my blogging with TextMate these past few weeks has been great, except for one terribly annoying thing, namely the inability to quickly conjure up a "linked-list"-type post. I was having to manually -- *gasp* -- create these (usually) quick, simple posts. I don't necessarily need speed when starting long-form blog posts like the one you're currently reading, but for the more frequent linked-list posts I definitely do. I need/want something that amounts to little more than a keyboard shortcut. MarsEdit has spoiled me over the years with its simple bookmarklet that grabs the data I need (i.e., page title, page URI and any currently-selected text), drops it into a new post (see my MarsEdit template below, formatted for Markdown) and populates the Title field with the title of the page.

[pageTitle](pageURI).

> selectedText

(Via []().)

Obviously then I wanted an equal or less amount of friction when doing similar operations via TextMate. (For long-form stuff I created a new blogging template that has all the fields I need for a non-linked-list post, so I can just go to File > New From Template > Blogging > Blog when I want to start writing a longer piece). While I was able to get some of the aforementioned bookmarklet's functionality in Chrome via a combination of (a service using) Automator and AppleScript (i.e., Service receives selected text in Google Chrome together with some AppleScript to act on the selected text), it was limited to just the highlighted text (i.e., no page title, URI, etc.), which made the process only slightly better (if not worse) than just doing everything manually. It was at this point I resigned trying to get this to work with Chrome and moved on to Safari/WebKit.

Because Safari's AppleScript support allows you to run JavaScript, getting the three data elements I wanted was quite easy. To wit:

tell application "WebKit"
    set selectedText to (do JavaScript "(getSelection())" in document 1)
    set pageURI to (get URL of document 1)
    set pageTitle to (do JavaScript "document.title" in document 1)
end tell

(Note: if you use Safari you'll want to change "WebKit" to "Safari.")

Once I had the critical information, I needed to figure out how to get all of it into a new post within TextMate. If you watched the blogging bundle screencast or previously have blogged with TextMate, you know that various post-specific elements are set in a "header" at the top of each file (where each file corresponds to a particular post). For my linked-list posts, this header looks like the following:

Title:
Slug:
Pings: Off
Comments: Off
Category: bits

To automate the inclusion of these headers in a new file (to be created when the script is set in motion), I had to create a new template for the blogging bundle (Bundles > Bundle Editor > Show Bundle Editor). (You can use the blogging templates that exist already as guides to creating your own.) After setting that up, I whipped up the following code to "click" on the correct menu item within TextMate so that a new file gets created using the new template.

tell application "TextMate"
    activate
    tell application "System Events"
        tell process "TextMate"
            tell menu bar 1
                tell menu bar item "File"
                    tell menu "File"
                        tell menu item "New From Template"
                            tell menu "New From Template"
                                tell menu item "Blogging"
                                    tell menu "Blogging"
                                        click menu item "Template"
                                    end tell
                                end tell
                            end tell
                        end tell
                    end tell
                end tell
            end tell
        end tell
    end tell
end tell

("Template" in the above code is whatever you named the template you created.) It's ugly, I know, but it gets the job done (and besides, I ended up not using it; keep reading). Now, each time I invoke the script I get a new file with proper headers and set to the desired bundle type (in my case, Markdown). However, I ran into a slight problem when attempting to insert the title, URI and selected text at their correct positions within the file (e.g., pageTitle should be inserted as so: "Title: pageTitle"). I tried using AppleScript to move around within the document (using variations on the code below, which was to move the cursor to the right by seven spaces), but no dice. I struggled for a while with this and ultimately just had to convince myself that a solution probably wasn't worth the time (though I'm certain it's possible).

repeat 7 times
    keystroke (ASCII character 129) using command down
end repeat

In light of my unsuccessful cursor positioning, I attempted to tackle the problem from another angle, and decided I could just create the headers with AppleScript instead of relying on the template from the blogging bundle. At this point there were three ways to move forward: 1) create a blank template and use it as described above (minus headers); 2) use TextMate's built-in URL scheme that allows you to open local files with TextMate from within other applications (e.g., Safari via AppleScript); or 3) use AppleScript to cause TextMate to open a local file directly.

Option 1 should be fairly self-explanatory given the discussion so far. For option 2 you'll want to tell Safari to run the following command within your script.

open location "txmt://open/?url=file://~/path/to/local/file"

The "local" file referenced above is one you will have created and saved, and to which you will have assigned the desired bundle type so that the syntax highlighting is correct. You'll want to keep this file empty, because each time you invoke your script the proper headers and other relevant information will be auto-inserted. In my case, I never need to save this file (i.e., when creating a new post) because I'm only using this setup for my linked-list posts, which I publish almost as soon as I create; given this, the file always is blank, and therefore ready to receive the stuff I want to insert.

Option 3 is the route I chose for my implementation, because it's the most straightforward (and because option 2 creates a new blank tab that I didn't feel like figuring out how to kill). With option 3 you simply tell TextMate to open the local file.

open "/path/to/local/file"

At this point all that was left to figure out was how to actually insert the headers and all of the information grabbed using JavaScript (discussed at the beginning of this piece). It ended up being rather trivial and is shown below as part of the "complete" AppleScript file, which you should be able to use outright (being sure, of course, to change the open file path).

tell application "WebKit"
    set selectedText to (do JavaScript "(getSelection())" in document 1)
    set pageURI to (get URL of document 1)
    set pageTitle to (do JavaScript "document.title" in document 1)
end tell

property LF : ASCII character 10

tell application "TextMate"
    activate
    open "/path/to/local/file"
    set post to "Title: " & pageTitle & LF
    set post to post & "Slug: " & LF
    set post to post & "Pings: Off" & LF
    set post to post & "Comments: Off" & LF
    set post to post & "Category: posts" & LF & LF
    set post to post & "[" & pageTitle & "]"
    set post to post & "(" & pageURI & ")." & LF & LF
    set post to post & "> " & selectedText & LF
    insert post
end tell

("LF" corresponds to the Unix linefeed character.)

I launch the script using FastScripts (specifically, I use option-b), but obviously you can just access the Script menu in the menu bar if you're OK with being a little less efficient. ;)

As I noted at the beginning of this piece, this solution works beautifully in Safari/WebKit, but I currently use Chrome. Hopefully Chrome's AppleScript support soon will be as robust as Safari's, but something tells me that that probably is the last thing on the team's mind right now. If and when sufficient support is there, I'll be sure to update this code and this post.

You should follow me on Twitter here