Viewing page 1 of 32

Writing Your First Sublime Plugin [link]

Written by Adam Presley on 10/22/2014 at 08:30 PM

Earlier this year I wrote a guest post for the web site sublimetexttips.com. This post walks you through your first time writing a plugin for Sublime Text, the most superb text and code editor. Sublime plugins are written in Python. If you've never written any Python before don't let it stop you. Python is an easy language to get into and you will find the experience most rewarding. So if you've ever considered writing your own plugin for Sublime Text read my article over at http://sublimetexttips.com/sublime-plugins-101-how-to-write-your-own-html5-template-plugin/. Cheers, and happy coding!

MailSlurper 4.0 Released

Written by Adam Presley on 06/05/2014 at 02:40 AM

MailSlurper 4.0 has just been released. This update addresses some bugs in parsing attachments and date display. It also adds the ability to choose from one of the following storage engines:

  • SQlite
  • MySQL
  • Microsoft SQL Server

This enhancement is one of several planned to make MailSlurper not only useful to individuals developing locally, but also to development teams in shared environments. So go grab the latest from Github!

MailSlurper 4.0

Happy coding!

MailSlurper 3.5 Released

Written by Adam Presley on 06/02/2014 at 10:21 PM

I have just published a small but important update to MailSlurper. Previously mails with attachments would cause issues with the parser and would not display correctly in the administrator. This has been addressed. In fact you can now see that a mail has attachments and even view them! So go get the latest version 3.5 on Github. Happy coding!

Parsing Woes in MailSlurper

Written by Adam Presley on 05/30/2014 at 03:37 AM

Tonight as I was working a bug in MailSlurper I quickly came to the realization that my parsing routine sucks. The current version is passable for simple text emails or basic HTML emails. But the moment you add attachments or inline images it all goes downhill. The further I dove into this problem the more I also realized that RFC 2822, or Internet Message Format and MIME are also a little dated and weird.

Basically a mail is broken down into two major parts. The first part is headers. Headers are key/value pairs separated by a colon, each set separated by a carriage return and line feed (\r\n for you nerds out there). This describes what to expect, such as the subject of the message, the sender, recipients, and more. After that another set of \r\n indicates we are ready for the second half of the mail, which is called the body. This contains the content of your email message. It also houses an HTML version of your mail if you used fancy things like links and italics, and it will also have Base64 encoded data for each attachment in your email.

To separate the mail message from the attachments the MIME specification defines what is called a boundary. This is an identifier in a specific format that comes before each attachment or section of the body. In learning all this though I started to see some complexity creep in. When the email is multipart, or contain attachments or pretty body content (HTML), the boundary gets slapped in on the end of the Content-Disposition header. So if Content-Disposition had a value already, it now has a semicolon, then the boundary marker definition.

Attachments commit the same evil in their version of the Content-Disposition header by having the file name follow after a semicolon. The is unfortunate because it muddies up my header parsing piece. I now have to be on the lookout for specific headers having additional information, which changes how I need to parse the body. If there is no boundary marker then I don't have any attachments or HTML to pull out. If I do, then parsing headers for attachments I have to look at that SAME header name of Content-Disposition but look for a different key for the file name. Ick.

There isn't really a point to this post except for me to rant and organize my thoughts a bit. This won't be hard to do, but it will take a bit of reogranizing my parsing code.

Only Saving When File Is Modified

Written by Adam Presley on 05/15/2014 at 01:48 PM

I got a ticket the other day questioning why my Sublime Text View In Browser plugin saves the file they are viewing every time the plugin is used. The answer of course is because it must be saved prior to opening in the browser. However the poster of the ticket did have a good point. There is no need to save the user's file if the file hasn't actually changed. I have pushed a modification to the plugin to ensure that the save only occurs if the user's file has any modifications.

To do this in Sublime's API turned out to be very simple.

if self.view.is_dirty():
    self.view.window().run_command("save")

The view object gives my plugin insight into the current view, or file being worked on. Sublime provides a nice method named is_dirty() which will tell me if the current view has any pending modifications. If it does I perform the save command. If not the rest of the plugin runs and your file is opened in your browser of choice.

Google Analytics Integrated Into Texo Dashboard

Written by Adam Presley on 05/13/2014 at 04:11 AM

Tonight I have integrated a few reports from Google Analytics into the Texo adminstrator Dashboard. I have included a Visits vs New Visits, Browser Stats, and Page Traffic information. Now I don't have to log in to the Google Analytics dashboard unless I really want to dig into stats. Most often I just want to see page views and what posts are getting the most hits.

Google Analytics Screenshot

To put these graphs on my dashboard I opted to use a 3rd party library called OOCharts. Their service hooks up to your Google account and provides a simplified API for retrieving charts and chart data. For example the code the retrieve the Browser usage pie chart looks like this.

oo.setAPIKey("MyAPIKey");
oo.load(function() {
    var
       browsers = new oo.Pie("MyProfileId", "30d");

    browsers.setMetric("ga:visits", "Visits");
    browsers.setDimension("ga:browser");

    browsers.draw("browserChart");
});

When you include the oocharts.js file on your page, or inject it using RequireJS in my case, you get a global variable called oo to use for interacting with the OOCharts API. The first activity you must perform is to initialize the library with your API key. An API key is retrieved from the OOCharts site when you sign up and link to your Google account. After this you need to call the load() function and provide it a callback function in which you can do all your chart initialization.

In our callback function we are setting up a new pie chart by initializing the constructor to the Pie object, passing in a profile ID and the duration of time in which to report on. In this example we are getting 30 days worth of data. Profile ID is another piece of data retrieved from either the OOCharts site or the Google Analytics dashboard. Next I am telling the Pie instance that I want to get visitor data (ga:visits) and report against browser usage (ga:browser). The final step is to call the draw() method passing in the ID of the div that will contain my chart.

OOCharts is a quick way to get started getting Google Analytics in your web application. I am impressed with how easy it was to get started. And now my dashboard has pretty charts! Yay, and happy coding!

Exporting Blog Entries as Markdown

Written by Adam Presley on 05/06/2014 at 05:25 AM

Since I write my blog posts in Markdown format I decided I wanted a way to backup my posts in a way that is human readable. The result is a feature in Texo to export my blog entries as zipped up Markdown files.

Screenshot of Export Interface

When I click on the button in the above screenshot all my blog entries are saved as Markdown files, zipped up, and presented to me as a download. The final zip is organized into folders of YEAR/MONTH.

Export Zip File

The code to do this is pretty simple. First I have a function that constructs the Markdown given a post dictionary.

def generateMarkdownFile(post):
   result = """Title: %s
Date: %s
Author: %s
Status: %s
Tags: %s
Slug: %s

%s""" % (
         post["title"],
         post["publishedDateTime"],
         post["author"],
         post["status"],
         post["tagList"],
         post["slug"],
         post["content"],
      )

   filename = os.path.join(config.UPLOAD_PATH, post["slug"]) + ".md"

   with open(filename, "w") as blogFile:
      blogFile.write(result)

   return filename

This function writes out a file to my temporary upload path with the post data converted to a format that Texo actually knows how to import. It is simple and human readable. The next part was to write a controller action to get my posts, write Markdown files, then zip them up and serve. This method is a bit big and needs a bit of refactoring, but it does the job for now.

@route("/admin/utilities/exportmarkdownfiles", method="GET")
@route("/admin/utilities/exportmarkdownfiles", method="POST")
@view("admin-export-markdown-files.html")
@requireSession
def adminExportMarkdownFiles():
   logger = logging.getLogger(__name__)

   if "btnExport" in request.all:
      posts = postservice.getAllPosts()
      zipfilePath = os.path.join(config.UPLOAD_PATH, "blog-posts.zip")

      zf = zipfile.ZipFile(zipfilePath, mode="w")
      filenames = []

      try:
         #
         # Write each post to a file, adding each file to a ZIP
         #
         for post in posts:
            filename = postservice.generateMarkdownFile(post=post)
            filenames.append(filename)

            writtenFilename = "%s/%s/%s" % (post["publishedYear"], post["publishedMonth"], os.path.basename(filename),)
            zf.write(filename, compress_type=compression, arcname=writtenFilename)

      except Exception as e:
         logger.error("There was an error writing zipfile: %s", e.message)

      finally:
         zf.close()

      #
      # Clean out markdown files
      #
      for filename in filenames:
         try:
            os.remove(filename)
         except Exception as e:
            logger.error("Unable to remove %s" % (filename,))

      #
      # Serve up the ZIP file as a download
      #
      return static_file(os.path.basename(zipfilePath), root=config.UPLOAD_PATH)

   return {
      "title": "Export Markdown Files"
   }

Basically this method starts up a zipfile output, gets all posts, and creates Markdown files for each post, adding to the zip file after each Markdown file is created. At the end I clean out all the generated Markdown files and serve the ZIP file back as a static download.

Also, as a final note, I added a fun easter-egg to my site. Try out the old Konami code on the home page. :) Cheers!

MailSlurper 3.0 Released

Written by Adam Presley on 05/05/2014 at 05:23 AM

I am proud to announce the 3.0 release of MailSlurper, the handy local development mail server that slurps mail into oblivion. When writing application that send mail I use MailSlurper to capture outgoing mail to ensure mail is working, and the actual mail item is what I expect it to be. MailSlurper is written in Google's Go language, with an administrator written in lots of JavaScript. This release includes:

  • Mails that contain HTML or are multipart text and HTML now display HTML in the viewer
  • Added ability to search the subject and bodies of mails to filter mail list
  • Added sorting of mail items
  • Addressed a date parsing issue with mails that have the timezone wrapped in parentheses
  • Addressed browser resize issue. Layout now is resizable and more responsive
  • Removed unneeded code
  • Updated several libraries

You can get the new release at the Github site.

MailSlurper 3.0

Added Amazon S3 Browser Widget to Texo

Written by Adam Presley on 04/18/2014 at 05:55 AM

After much work I have completed adding an Amazon S3 Browser widget to Texo CMS. Texo is the engine that I wrote which runs this blog. It is still in its infancy but this widget will certainly make writing my blog posts easier. After setting up Amazon S3 key settings and bucket information a new button shows up while editing or writing blog entries. Clicking on this brings up a widget that shows all available images and allows me to view them, select an image, delete, and even upload new images. Selecting an image will populate the correct Markdown syntax in the editor with the URL ready to go.

Selecting an image

Uploading an image

Deleting an image

JavaScript Functional Goodness Part 3

Written by Adam Presley on 04/17/2014 at 05:17 AM

More and more I am trying to stretch my "functional muscles" any time I have a chance. I've been working on an enhancement to my blog engine by adding a button to the editor I am using for editing my Markdown-based posts. This new button, when clicked, will display a dialog widget with thumbnails for all images in an Amazon S3 bucket.

S3 Browser Screenshot

When the widget is initialized it makes a call to a RESTful endpoint that is responsible for authenticating credentials and retrieving bucket contents. The result of that call returns data that looks something like this.

[
    {"url":"http://adampresley.com/image1.png", "name": "image1.png"},
    {"url":"http://adampresley.com/image2.png", "name": "image2.png"},
    {"url":"http://adampresley.com/image3.png", "name": "image3.png"},
    {"url":"http://adampresley.com/image4.png", "name": "image4.png"}
]

Originally I had the code perform an AJAX call using jQuery and then did a good old-fashioned for loop over that data. Inside that loop I was concatenating to a string the DOM necessary to create image elements to display thumbnails of each image. Then I decided to try to clean it up and go a little functional on it.

The first order of business we must address is to define a function that will create the DOM element for a single thumbnail image given a single item in the array demonstrated above. This code creates an image element and attaches the src attribute which is the URL to the image. It also stores the S3 key name in the attribute data-name for retrieval later on. Then we setup some CSS attributes to get it sized right and return our new element. That looks like this.

_createThumbnailItemDom = function(thumbnailItem) {
    return $("<img />")
        .attr("src", thumbnailItem.url)
        .attr("data-name", thumbnailItem.name)
        .addClass("s3BrowserWidgetItem")
        .css({
            width: "150px",
            height: "150px",
            margin: "15px"
        });
}

Next up we need a method to iterate over each item in our returned dataset and craft the DOM elements, then attach them to a DIV container. This problem turned out to be a one-liner, but has two real parts to it. The first part of this is transforming the current object in our array to a DOM element using our method defined above. We can use the $.map() method in jQuery to apply a function to all items in our array like so.

$.map(data, _createThumbnailItemDom)

The above line of code will return a new array with each item in the old array transformed into DOM elements. Now we need to append all those elements to a target element. So what we will do is create a method that takes a reference to the target element and the array of AJAX data. We then will iterate over that dataset using jQuery's $.each() method. As you may recall the $.each() method takes two arguments. The first is an array, and the second is a function that takes an index and a current item. In this function you do whatever you wish with the item. In our case, we want to append it to the target DOM element.

_createThumbnailsDom = function(targetEl, data) {
    $.each($.map(data, _createThumbnailItemDom), function(index, el) { targetEl.append(el); });
}

The final step is to make an AJAX call to get our data, then on successful return call our _createThumbnailsDom() method to create our thumbnails.

var onSuccess = function(response) { _createThumbnailsDom($(el), response.data); };
$.ajax({ url: "/some/endpoint/for/bucketdata" }).done(onSuccess);

And that's it! Below is the full code for the S3 Browser widget so far. I'm sure it will change over time, but this is how it looks now. Cheers, and happy coding!

/**
 * Class: S3Browser
 * This class provides a visual widget for viewing files in an Amazon
 * S3 bucket. A user can select images to get a full URL to the
 * S3 location. This window also allows uploading files to Amazon S3.
 *
 * This widget is based on the jQuery UI dialog widget. As such all the
 * same options available to the dialog widget are available in the S3Browser
 * widget.
 *
 * When an image is selected in the S3 Browser an event labeled 
 * *s3browser-widget.select* is fired.
 *
 * Exports:
 *    $.ui.S3Browser
 *
 * RequireJS Name:
 *    s3browser-widget
 *
 * Dependencies:
 *    jquery
 *    jqueryui
 *
 * Commands:
 *    open - Opens the S3 Browser
 *    close - Closes the S3 Browser
 *
 * Example:
 *    > require(["jquery", "s3browser-widget"], function($) {
 *    >    $("#someDiv").S3Browser();
 *    >    $("#someDiv").S3Browser("open");
 *    > });
 */
define(
    [
        "jquery", "rajo.pubsub", "jqueryui"
    ],
    function($, PubSub) {
        "use strict";

        var 
            /**
             * Fuction: _createDom
             * Method that creates the DOM for a single instance of this dialog widget.
             *
             * Parameters:
             *    getBucketListEndpoint - URL to the service endpoint to get the list of items in an S3 bucket
             *    dialogEl              - A reference to the dialog element to render to
             *    dialogElId            - ID of the dialog element to render to
             */
            _createDom = function(getBucketListEndpoint, dialogEl, dialogElId) {
                var
                    body = "<div class=\"s3BrowserWidgetItems\" style=\"width: 100%; height: auto;\"></div>",
                    el = "#" + dialogElId + " .s3BrowserWidgetItems",

                    onSuccess = function(response) { _createThumbnailsDom($(el), response.data); };

                $.ajax({ url: getBucketListEndpoint }).done(onSuccess);

                /*
                 * Add the initial body container to the dialog. The thumbnails
                 * are loaded via AJAX and attached via the _createThumbnailsDom method.
                 */
                dialogEl.html(body);

                /*
                 * Assign a click event handler to any element with a class of 
                 * "s3BrowserWidgetItem" that is a child of our container element.
                 */
                $(el).on("click", ".s3BrowserWidgetItem", function() {
                    $(el + " .s3BrowserWidgetItem").removeClass("img-thumbnail");
                    $(this).toggleClass("img-thumbnail");
                });
            },

            /**
             * Function: _createThumbnailItemDom
             * Creates an individual thumbnail DOM item and returns it.
             * 
             * Parameters:
             *    thumbnailUrl - URL to the image thumbnail
             */
            _createThumbnailItemDom = function(thumbnailItem) {
                return $("<img />")
                    .attr("src", thumbnailItem.url)
                    .attr("data-name", thumbnailItem.name)
                    .addClass("s3BrowserWidgetItem")
                    .css({
                        width: "150px",
                        height: "150px",
                        margin: "15px"
                    });
            },

            /**
             * Function: _createThumbnailsDom
             * This function creates all thumbnail DOM elements in a set of data
             * and appends them to a target DOM element. The data parameter
             * is an array of thumbnail URLs.
             *
             * Parameters:
             *    targetEl - Element to attach thumbnail DOM items to
             *    data     - Array of thumbnail URLs
             */
            _createThumbnailsDom = function(targetEl, data) {
                $.each($.map(data, _createThumbnailItemDom), function(index, el) { targetEl.append(el); });
            },

            /**
             * Function: _onDelete
             * Event handler for the *Delete* button. This will publish an event named
             * *s3browser-widget.delete* with a reference to the dialog element, the
             * URL of the image, and the S3 key name.
             */
            _onDelete = function(dialogEl) {
                var selectedImageEl = $(dialogEl).find(".s3BrowserWidgetItem.img-thumbnail");

                if (selectedImageEl.length > 0) {
                    PubSub.publish("s3browser-widget.delete", {
                        dialogEl: dialogEl,
                        imageUrl: selectedImageEl[0].src,
                        name    : selectedImageEl[0].getAttribute("data-name")
                    });
                }
            },

            /**
             * Function: _onSelect
             * Event handler for the *Select* button. This will publish an event
             * named *s3browser-widget.select* with a reference to the dialog element, the
             * URL of the image, and the S3 key name.
             */
            _onSelect = function(dialogEl) {
                var selectedImageEl = $(dialogEl).find(".s3BrowserWidgetItem.img-thumbnail");

                if (selectedImageEl.length > 0) {
                    PubSub.publish("s3browser-widget.select", {
                        dialogEl: dialogEl,
                        imageUrl: selectedImageEl[0].src,
                        name    : selectedImageEl[0].getAttribute("data-name")
                    });
                }
            },

            /**
             * Function: _onView
             * Event handler for the *View* button. This will open up the selected
             * image in a new tab/window.
             */
            _onView = function(dialogEl) {
                var selectedImageEl = $(dialogEl).find(".s3BrowserWidgetItem.img-thumbnail");

                if (selectedImageEl.length > 0) {
                    window.open(selectedImageEl[0].src);
                }
            };

        /*
         * Create the widget in the "adampresley" namespace using the 
         * jQuery UI WidgetFactory.
         */
        $.widget("adampresley.S3Browser", $.ui.dialog, {
            _create: function() {
                _createDom(this.options.getBucketListEndpoint, this.element, this.element[0].id);
                this._super();
            },

            options: {
                title   : "Amazon S3 Browser",
                width   : 450,
                height  : 450,
                autoOpen: false,
                modal   : true,
                resizable: true,
                buttons : [
                    {
                        text : "Select",
                        click: function() { _onSelect(this); }
                    },
                    {
                        text : "View",
                        click: function() { _onView(this); }
                    },
                    {
                        text : "Delete",
                        click: function() { _onDelete(this); }
                    }
                ],

                getBucketListEndpoint: "/admin/ajax/s3/bucket"
            }
        });
    }
);

Tags