<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-2177473232402653333</id><updated>2011-11-06T19:08:40.722-08:00</updated><category term='mobile'/><category term='ruby'/><category term='shell script'/><category term='minification'/><category term='cryptography'/><category term='ecmascript'/><category term='javascript'/><category term='second-guessing'/><category term='documentation'/><category term='crockford'/><category term='security'/><category term='smalltalk'/><category term='css queries'/><category term='jaro-winkler'/><category term='DRY'/><category term='music synthesis'/><category term='cssx'/><category term='algorithm'/><category term='syntax'/><category term='blog'/><category term='date formatter'/><category term='compression'/><category term='meta'/><category term='DOM'/><category term='iphone'/><category term='economics'/><category term='supercollider'/><category term='string matching'/><category term='css'/><category term='python'/><category term='html'/><category term='Objective-C'/><category term='internet'/><category term='history'/><category term='templating'/><category term='OOP'/><category term='Cocoa'/><category term='code'/><category term='testing'/><category term='JSON'/><category term='satire'/><category term='usability'/><category term='compiler'/><category term='google'/><title type='text'>David Golightly - Patterns</title><subtitle type='html'>Software development and existential drama</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>22</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-4054848093721016895</id><published>2009-03-03T19:01:00.001-08:00</published><updated>2009-03-04T13:42:22.947-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='mobile'/><category scheme='http://www.blogger.com/atom/ns#' term='jaro-winkler'/><category scheme='http://www.blogger.com/atom/ns#' term='algorithm'/><category scheme='http://www.blogger.com/atom/ns#' term='string matching'/><title type='text'>Jaro-Winkler implementation in Ruby for User-Agent matching</title><content type='html'>&lt;p&gt;Working in the mobile web, it's hard to keep up with the constant glut of devices on the market and their rapidly evolving capabilities.  Of course, progressive enhancement is key; the markup you use in a mobile site should be minimal and look good without any CSS, JavaScript, or images, which is the worst-case.  But we don't want to stop there; we want our sites to look as good as they can on as many mobile devices as possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://wurfl.sourceforge.net/"&gt;WURFL&lt;/a&gt; is here to help with that.  A crowd-sourced database of mobile devices, updated regularly, WURFL gives you detailed information on the capabilities of over 10,000 mobile devices indexed by user-agent string and grouped by manufacturer.  It's relatively straightforward to set up a cronjob for a rake task that downloads an updated WURFL file and stores it in a table in your database; using an ActiveRecord model, you can then query the data provided by WURFL to get all the details for any device which has a user-agent string in the database.&lt;/p&gt;

&lt;p&gt;But device makers throw a wrench in these efforts: user-agent strings are frequently updated or modified from their canonical form without rhyme or reason.&lt;/p&gt;

&lt;p&gt;Fortunately, we have the &lt;a href="http://en.wikipedia.org/wiki/Jaro-Winkler_distance"&gt;Jaro-Winkler distance algorithm&lt;/a&gt;, which gives us a quick-and-dirty matching algorithm for strings that are mostly the same but may vary in arbitrary ways.  Using this algorithm, we can run indexed queries for exact matches, then pull in some subset (perhaps matching the first 20 characters of the mystery UA string), iterate over these devices, and find the best match to return.  Failing to find any implementation of the Jaro-Winkler algorithm in Ruby, I had to cobble together my own; I hope you find it useful.&lt;/p&gt;

&lt;pre&gt;
&lt;span class="PreProc"&gt;module&lt;/span&gt; &lt;span class="Type"&gt;JaroWinkler&lt;/span&gt;

    &lt;span class="PreProc"&gt;def&lt;/span&gt; &lt;span class="Constant"&gt;self&lt;/span&gt;.&lt;span class="Identifier"&gt;distance&lt;/span&gt;(str1, str2)
      str1.strip!
      str2.strip!

      &lt;span class="Statement"&gt;if&lt;/span&gt; str1 == str2
        &lt;span class="Statement"&gt;return&lt;/span&gt; &lt;span class="Constant"&gt;1&lt;/span&gt;
      &lt;span class="Statement"&gt;end&lt;/span&gt;

      &lt;span class="Comment"&gt;# str2 should be the longer string&lt;/span&gt;
      &lt;span class="Statement"&gt;if&lt;/span&gt; str1.length &amp;gt; str2.length
        tmp = str2
        str2 = str1
        str1 = tmp
      &lt;span class="Statement"&gt;end&lt;/span&gt;

      lmax = str2.length

      &lt;span class="Comment"&gt;# arrays to keep track of positions of matches&lt;/span&gt;
      found1 = &lt;span class="Type"&gt;Array&lt;/span&gt;.new(str1.length, &lt;span class="Constant"&gt;false&lt;/span&gt;)
      found2 = &lt;span class="Type"&gt;Array&lt;/span&gt;.new(str2.length, &lt;span class="Constant"&gt;false&lt;/span&gt;)

      midpoint = ((str1.length / &lt;span class="Constant"&gt;2&lt;/span&gt;) - &lt;span class="Constant"&gt;1&lt;/span&gt;).to_i

      common = &lt;span class="Constant"&gt;0&lt;/span&gt;

      &lt;span class="Statement"&gt;for&lt;/span&gt; i &lt;span class="Statement"&gt;in&lt;/span&gt; &lt;span class="Constant"&gt;0&lt;/span&gt;..str1.length
        first = &lt;span class="Constant"&gt;0&lt;/span&gt;
        last = &lt;span class="Constant"&gt;0&lt;/span&gt;
        &lt;span class="Statement"&gt;if&lt;/span&gt; midpoint &amp;gt;= i
            first = &lt;span class="Constant"&gt;1&lt;/span&gt;
            last = i + midpoint
        &lt;span class="Statement"&gt;else&lt;/span&gt;
            first = i - midpoint
            last = i + midpoint
        &lt;span class="Statement"&gt;end&lt;/span&gt;

        last = lmax &lt;span class="Statement"&gt;if&lt;/span&gt; last &amp;gt; lmax

        &lt;span class="Statement"&gt;for&lt;/span&gt; j &lt;span class="Statement"&gt;in&lt;/span&gt; first..last
          &lt;span class="Statement"&gt;if&lt;/span&gt; str2[j] == str1[i] &lt;span class="Statement"&gt;and&lt;/span&gt; found2[j] == &lt;span class="Constant"&gt;false&lt;/span&gt;
            common += &lt;span class="Constant"&gt;1&lt;/span&gt;
            found1[i] = &lt;span class="Constant"&gt;true&lt;/span&gt;
            found2[j] = &lt;span class="Constant"&gt;true&lt;/span&gt;
            &lt;span class="Statement"&gt;break&lt;/span&gt;
          &lt;span class="Statement"&gt;end&lt;/span&gt;
        &lt;span class="Statement"&gt;end&lt;/span&gt;
      &lt;span class="Statement"&gt;end&lt;/span&gt;

      last_match = &lt;span class="Constant"&gt;1&lt;/span&gt;
      tr = &lt;span class="Constant"&gt;0&lt;/span&gt;
      &lt;span class="Statement"&gt;for&lt;/span&gt; i &lt;span class="Statement"&gt;in&lt;/span&gt; &lt;span class="Constant"&gt;0&lt;/span&gt;..found1.length
        &lt;span class="Statement"&gt;if&lt;/span&gt; found1[i]
          &lt;span class="Statement"&gt;for&lt;/span&gt; j &lt;span class="Statement"&gt;in&lt;/span&gt; (last_match..found2.length)
            &lt;span class="Statement"&gt;if&lt;/span&gt; found2[j]
              last_match = j + &lt;span class="Constant"&gt;1&lt;/span&gt;
              tr += &lt;span class="Constant"&gt;0.5&lt;/span&gt; &lt;span class="Statement"&gt;if&lt;/span&gt; str1[i] != str2[j]
            &lt;span class="Statement"&gt;end&lt;/span&gt;
          &lt;span class="Statement"&gt;end&lt;/span&gt;
        &lt;span class="Statement"&gt;end&lt;/span&gt;
      &lt;span class="Statement"&gt;end&lt;/span&gt;

      onethird = &lt;span class="Constant"&gt;1.0&lt;/span&gt;/&lt;span class="Constant"&gt;3&lt;/span&gt;
      &lt;span class="Statement"&gt;if&lt;/span&gt; common &amp;gt; &lt;span class="Constant"&gt;0&lt;/span&gt;
        &lt;span class="Statement"&gt;return&lt;/span&gt; [(onethird * common / str1.length) +
                (onethird * common / str2.length) +
                (onethird * (common - tr) / common), &lt;span class="Constant"&gt;1&lt;/span&gt;].min
      &lt;span class="Statement"&gt;else&lt;/span&gt;
        &lt;span class="Statement"&gt;return&lt;/span&gt; &lt;span class="Constant"&gt;0&lt;/span&gt;
      &lt;span class="Statement"&gt;end&lt;/span&gt;
    &lt;span class="PreProc"&gt;end&lt;/span&gt;

&lt;span class="PreProc"&gt;end&lt;/span&gt;

&lt;/pre&gt;

&lt;p&gt;This will return a value such that 0 &lt;= value &lt;= 1.  Matches near the beginning of the strings are weighted more heavily than matches near the end, making this a good algorithm to use with user-agent strings which are typically long and have minor variations toward the end.&lt;/p&gt;

&lt;p&gt;That said, this isn't necessarily the optimal method to detect user-agent strings, and since it can easily give false positives in cases where a desktop and a mobile version of the same browser exist, it should be used in conjunction with a whitelist of known desktop browsers (which, though substantial, is much shorter than the list of all mobile browsers).  So I offer this code with the disclaimer that Jaro-Winkler may not be the most effective algorithm for user-agent string matching.&lt;/p&gt;

&lt;p&gt;By the way, I'm just learning Ruby, so if I'm doing anything atrocious here please let me know.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-4054848093721016895?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/4054848093721016895/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=4054848093721016895' title='11 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4054848093721016895'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4054848093721016895'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2009/03/jaro-winkler-implementation-in-ruby-for.html' title='Jaro-Winkler implementation in Ruby for User-Agent matching'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>11</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-1994163174175250831</id><published>2009-02-25T22:11:00.000-08:00</published><updated>2009-11-26T13:48:05.284-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Objective-C'/><category scheme='http://www.blogger.com/atom/ns#' term='Cocoa'/><category scheme='http://www.blogger.com/atom/ns#' term='iphone'/><title type='text'>Asynchronous Image Caching with the iPhone SDK</title><content type='html'>&lt;p&gt;Coming from the web world, I'm used to not having to think about all the functionality implicit in the humble HTML &amp;lt;img&amp;gt; tag, including client-side resource caching and asynchronous loading.  In fact, the browser will cache any resource sent with the appropriate HTTP response headers, but by far the most common use of this cache behavior on most websites is images of all kinds.&lt;/p&gt;

&lt;p&gt;So in building a couple of simple &lt;a href="http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=303814652&amp;mt=8"&gt;iPhone&lt;/a&gt; &lt;a href="http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=305504330&amp;mt=8"&gt;apps&lt;/a&gt; recently that display a list of search results retrieved from a server call, one piece that was missing was an asynchronous image loader that retrieves and displays thumbnail images in UITableViewCells.&lt;/p&gt;

&lt;p&gt;[UIImage imageWithData:[NSData dataWithContentsOfURL:url]] gives you default iPhone HTTP caching behavior using NSURLCache, but it turns out that's not good enough: NSURLCache only stores URL contents in memory for the duration of the use of the application.  We also need a URL cache that writes results to disk so that frequently-accessed responses only, and it also needs to manage its capacity.  After that, we need an image loader that will abstract away the work of downloading images asynchronously, so that when a cell needs an image, it can request the image from the CachedImageLoader, and the CachedImageLoader will call back with the image, whether loaded from cache or remotely.

Let's start with the data cache, DiskCache, which simply caches NSData objects in files in the application's file path:

&lt;pre&gt;
&lt;span class="Type"&gt;extern&lt;/span&gt; &lt;span class="Type"&gt;const&lt;/span&gt; NSUInteger kMaxDiskCacheSize;

&lt;span class="Statement"&gt;@interface&lt;/span&gt; DiskCache : NSObject {
&lt;span class="Statement"&gt;@private&lt;/span&gt;
        NSString *_cacheDir;
        NSUInteger _cacheSize;
        NSUInteger _diskCapacity;
}

@property (nonatomic) NSUInteger diskCapacity;
@property (nonatomic, readonly) NSString *cacheDir;
@property (nonatomic, readonly) NSUInteger size;

&lt;span class="Identifier"&gt;+ &lt;/span&gt;(DiskCache *)sharedCache;

&lt;span class="Identifier"&gt;- &lt;/span&gt;(NSData *)dataInCacheForURLString:(NSString *)urlString;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)cacheData:(NSData *)data
                  request:(NSURLRequest *)request
                 response:(NSURLResponse *)response;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)clearCachedDataForRequest:(NSURLRequest *)request;


&lt;span class="Statement"&gt;@end&lt;/span&gt;
&lt;/pre&gt; 



&lt;p&gt;The implementation for this one will be pretty straightforward: it's a singleton class that obscures as much activity as possible, including the cache path and file naming scheme (the implementation actually just uses the original URL filename, though this is potentially brittle when resources with the same name are located in different paths).  One detail that I find interesting is determining folder size, which is less straightforward than I expected under Cocoa.  To determine the size of all files in a given directory, you have to walk the directory and add up their sizes:&lt;/p&gt;

&lt;style type="text/css"&gt;
&lt;!--
.Special { color: #ff40ff; }
.Statement { color: #ffff00; }
.Identifier { color: #00ffff; }
.Comment { color: #8080ff; }
.PreProc { color: #ff40ff; }
.Constant { color: #ff6060; }
.Type { color: #00ff00; }
--&gt;
&lt;/style&gt;
&lt;pre&gt;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(NSUInteger)size {
    NSString *cacheDir = [&lt;span class="Statement"&gt;self&lt;/span&gt; cacheDir];
    &lt;span class="Statement"&gt;if&lt;/span&gt; (_cacheSize &amp;lt;= &lt;span class="Constant"&gt;0&lt;/span&gt; &amp;amp;&amp;amp; cacheDir != &lt;span class="Constant"&gt;nil&lt;/span&gt;) {
        NSArray *dirContents = [[NSFileManager defaultManager]
                                directoryContentsAtPath:cacheDir];
        NSString *file;
        NSDictionary *attrs;
        NSNumber *fileSize;
        NSUInteger totalSize = &lt;span class="Constant"&gt;0&lt;/span&gt;;

        &lt;span class="Statement"&gt;for&lt;/span&gt; (file &lt;span class="Type"&gt;in&lt;/span&gt; dirContents) {
            &lt;span class="Statement"&gt;if&lt;/span&gt; ([[file pathExtension] isEqualToString:&lt;span class="Constant"&gt;@&amp;quot;jpg&amp;quot;&lt;/span&gt;]) {
                attrs = [[NSFileManager defaultManager]
                         fileAttributesAtPath:[cacheDir
                             stringByAppendingPathComponent:file]
                         traverseLink:NO];

                fileSize = [attrs objectForKey:NSFileSize];
                totalSize += [fileSize integerValue];
            }
        }

        _cacheSize = totalSize;
        DLog(&lt;span class="Constant"&gt;@&amp;quot;cache size is: &lt;/span&gt;&lt;span class="Special"&gt;%d&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;&lt;/span&gt;, _cacheSize);
    }
    &lt;span class="Statement"&gt;return&lt;/span&gt; _cacheSize;
}
&lt;/pre&gt;

&lt;p&gt;Here, I'm only interested in files with the "jpg" extension, though this is the only hard-coded reference to images in the entire implementation and could easily be changed.  Note the Objective-C syntax &lt;/p&gt;

&lt;pre&gt;&lt;span class="Statement"&gt;for&lt;/span&gt; (NSString *file &lt;span class="Type"&gt;in&lt;/span&gt; dirContents)&lt;/pre&gt;

&lt;p&gt;which is known as &lt;a href="http://developer.apple.com/DOCUMENTATION/Cocoa/Conceptual/ObjectiveC/Articles/chapter_8_section_1.html"&gt;fast enumeration&lt;/a&gt; and gets compiled down into pointer arithmetic, much faster than indexing using NSArray's objectForKey: method.  Perhaps there's a more concise way to perform this task, but I haven't run across it yet.&lt;/p&gt;

&lt;p&gt;While we're here, it's also worth taking a look at the mechanism for trimming the old files out of the disk cache when it reaches capacity.  This involves first filtering the directory contents, then sorting the filtered contents by date modified; welcome to the wonderful world of NSArray sort functions:&lt;/p&gt;

&lt;pre&gt;
NSInteger dateModifiedSort(&lt;span class="Type"&gt;id&lt;/span&gt; file1, &lt;span class="Type"&gt;id&lt;/span&gt; file2, &lt;span class="Type"&gt;void&lt;/span&gt; *reverse) {
    NSDictionary *attrs1 = [[NSFileManager defaultManager]
                            attributesOfItemAtPath:file1
                            error:&lt;span class="Constant"&gt;nil&lt;/span&gt;];
    NSDictionary *attrs2 = [[NSFileManager defaultManager]
                            attributesOfItemAtPath:file2
                            error:&lt;span class="Constant"&gt;nil&lt;/span&gt;];

    &lt;span class="Statement"&gt;if&lt;/span&gt; ((NSInteger *)reverse == &lt;span class="Constant"&gt;0&lt;/span&gt;) {
        &lt;span class="Statement"&gt;return&lt;/span&gt; [[attrs2 objectForKey:NSFileModificationDate]
                compare:[attrs1 objectForKey:NSFileModificationDate]];
    }

    &lt;span class="Statement"&gt;return&lt;/span&gt; [[attrs1 objectForKey:NSFileModificationDate]
            compare:[attrs2 objectForKey:NSFileModificationDate]];
}


&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)trimDiskCacheFilesToMaxSize:(NSUInteger)targetBytes {
    targetBytes = MIN([&lt;span class="Statement"&gt;self&lt;/span&gt; diskCapacity], MAX(&lt;span class="Constant"&gt;0&lt;/span&gt;, targetBytes));
    &lt;span class="Statement"&gt;if&lt;/span&gt; ([&lt;span class="Statement"&gt;self&lt;/span&gt; size] &amp;gt; targetBytes) {
        NSArray *dirContents = [[NSFileManager defaultManager]
                                directoryContentsAtPath:[&lt;span class="Statement"&gt;self&lt;/span&gt; cacheDir]];

        NSMutableArray *filteredArray = [NSMutableArray 
                                arrayWithCapacity:[dirContents count]];
        &lt;span class="Statement"&gt;for&lt;/span&gt; (NSString *file &lt;span class="Type"&gt;in&lt;/span&gt; dirContents) {
            &lt;span class="Statement"&gt;if&lt;/span&gt; ([[file pathExtension] isEqualToString:&lt;span class="Constant"&gt;@&amp;quot;jpg&amp;quot;&lt;/span&gt;]) {
                [filteredArray addObject:[[&lt;span class="Statement"&gt;self&lt;/span&gt; cacheDir]
                                stringByAppendingPathComponent:file]];
            }
        }

        NSInteger reverse = &lt;span class="Constant"&gt;1&lt;/span&gt;;
        NSMutableArray *sortedDirContents = [NSMutableArray arrayWithArray:
                                     [filteredArray
                                sortedArrayUsingFunction:dateModifiedSort
                                                                             context:&amp;amp;reverse]];
        &lt;span class="Statement"&gt;while&lt;/span&gt; (_cacheSize &amp;gt; targetBytes &amp;amp;&amp;amp; [sortedDirContents count] &amp;gt; &lt;span class="Constant"&gt;0&lt;/span&gt;) {
            &lt;span class="Type"&gt;id&lt;/span&gt; file = [sortedDirContents lastObject];
            NSDictionary *attrs = [[NSFileManager defaultManager]
                                   attributesOfItemAtPath:file
                                   error:&lt;span class="Constant"&gt;nil&lt;/span&gt;];
            _cacheSize -= [[attrs objectForKey:NSFileSize] integerValue];
            [[NSFileManager defaultManager] removeItemAtPath:file
                                                       error:&lt;span class="Constant"&gt;nil&lt;/span&gt;];
            [sortedDirContents removeLastObject];
        }
    }
}

&lt;/pre&gt;

&lt;p&gt;Next, we take a look at the CachedImageLoader, which uses a protocol ImageConsumer to for its clients; this keeps us from being locked into any particular image renderer (such as UITableViewCells or what have you); in fact, it's used in a few different places in the GoTime apps:&lt;/p&gt;

&lt;pre&gt;&lt;span class="Statement"&gt;@protocol&lt;/span&gt; ImageConsumer &amp;lt;NSObject&amp;gt;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(NSURLRequest *)request;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)renderImage:(UIImage *)image;
&lt;span class="Statement"&gt;@end&lt;/span&gt;


&lt;span class="Statement"&gt;@interface&lt;/span&gt; CachedImageLoader : NSObject {
&lt;span class="Statement"&gt;@private&lt;/span&gt;
        NSOperationQueue *_imageDownloadQueue;
}


&lt;span class="Identifier"&gt;+ &lt;/span&gt;(CachedImageLoader *)sharedImageLoader;


&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)addClientToDownloadQueue:(&lt;span class="Type"&gt;id&lt;/span&gt;&amp;lt;ImageConsumer&amp;gt;)client;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(UIImage *)cachedImageForClient:(&lt;span class="Type"&gt;id&lt;/span&gt;&amp;lt;ImageConsumer&amp;gt;)client;

&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)suspendImageDownloads;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)resumeImageDownloads;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)cancelImageDownloads;


&lt;span class="Statement"&gt;@end&lt;/span&gt;
&lt;/pre&gt;

&lt;p&gt;Again, this is a singleton class, which is basically a wrapper for NSOperationQueue, which is a really handy high-level threading implementation.  (There are &lt;a href="http://www.mikeash.com/?page=pyblog/dont-use-nsoperationqueue.html"&gt;reports&lt;/a&gt; that NSOperationQueue is broken on OS X Leopard, but the single-core chip architecture on the iPhone avoids the cause of this bug, so it can be safely used; indeed, I haven't had any problems with it.)  NSOperationQueue allows you to add NSOperations, which get run &lt;a href="http://en.wikipedia.org/wiki/FIFO"&gt;FIFO&lt;/a&gt; on a number of threads specified in maxConcurrentOperationCount.  I've found that a single image download thread is sufficient, even on edge networks: the network speed is always the bottleneck, not lack of concurrency.  This allows the implementation of CachedImageLoader to be single threaded (counting the operation thread) and use synchronous downloading.  A image consumer needing an image implements the ImageConsumer protocol, then calls addClientToDownloadQueue: passing itself as the argument:&lt;/p&gt;

&lt;pre&gt;
&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)addClientToDownloadQueue:(&lt;span class="Type"&gt;id&lt;/span&gt;&amp;lt;ImageConsumer&amp;gt;)client {
    UIImage *cachedImage;
    &lt;span class="Statement"&gt;if&lt;/span&gt; (cachedImage = [&lt;span class="Statement"&gt;self&lt;/span&gt; cachedImageForClient:client]) {
        [client renderImage:cachedImage];
    } &lt;span class="Statement"&gt;else&lt;/span&gt; {
        NSOperation *imageDownloadOp = [[[NSInvocationOperation alloc] 
                        initWithTarget:&lt;span class="Statement"&gt;self&lt;/span&gt;
                              selector:&lt;span class="Statement"&gt;@selector&lt;/span&gt;(loadImageForClient:)
                                object:client] autorelease];
        [_imageDownloadQueue addOperation:imageDownloadOp];
    }
}

&lt;span class="Identifier"&gt;- &lt;/span&gt;(&lt;span class="Type"&gt;void&lt;/span&gt;)loadImageForClient:(&lt;span class="Type"&gt;id&lt;/span&gt;&amp;lt;ImageConsumer&amp;gt;)client {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    &lt;span class="Statement"&gt;if&lt;/span&gt; (![&lt;span class="Statement"&gt;self&lt;/span&gt; loadImageRemotelyForClient:client]) {
        [&lt;span class="Statement"&gt;self&lt;/span&gt; addClientToDownloadQueue:client];
    }

    [pool release];
}

&lt;/pre&gt;

&lt;p&gt;In cachedImageForClient, CachedImageLoader uses both the in-memory NSURLCache as well as our DiskCache class above, in turn.  Failing to find the image in the cache, the loader creates an NSInvocationOperation which, in turn, gets called on our download thread; we create an autorelease pool and download the image synchronously, caching the image data when we have it (in both DiskCache and NSURLCache.)&lt;/p&gt;

&lt;p&gt;So there you have it, most of an implementation of an asynchronous, caching image downloader for iPhone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UPDATE 11/26/2009:&lt;/strong&gt; The complete source code for the cache is available &lt;a href="http://www.davidgolightly.net/code/cache.tar.gz"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-1994163174175250831?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/1994163174175250831/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=1994163174175250831' title='22 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1994163174175250831'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1994163174175250831'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2009/02/asynchronous-image-caching-with-iphone.html' title='Asynchronous Image Caching with the iPhone SDK'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>22</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-5856505916606127429</id><published>2009-02-24T22:49:00.000-08:00</published><updated>2009-02-26T00:08:35.972-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='smalltalk'/><category scheme='http://www.blogger.com/atom/ns#' term='music synthesis'/><category scheme='http://www.blogger.com/atom/ns#' term='documentation'/><category scheme='http://www.blogger.com/atom/ns#' term='supercollider'/><category scheme='http://www.blogger.com/atom/ns#' term='code'/><category scheme='http://www.blogger.com/atom/ns#' term='OOP'/><title type='text'>Cracking Supercollider</title><content type='html'>&lt;p&gt;&lt;a href="http://www.audiosynth.com/"&gt;SuperCollider&lt;/a&gt; is an amazing language, if you are in the super genius club who can understand the extensive documentation or pursue graduate studies at &lt;a href="http://ccrma.stanford.edu/"&gt;CCRMA&lt;/a&gt;.  I've been out of audio synth for a while, but recently found myself needing to apply a recursive delay line to an audio recording I'm working on for my music project, &lt;a href="http://www.myspace.com/middayveil"&gt;Midday Veil&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;SuperCollider dates from the mid-90's, and is thus a peer to Ruby, sharing influences like Smalltalk and Lisp.  As languages go, it's pretty nice, though the compiler leaves something to be desired - inexplicably, variables must be declared before use at the top of a function, much like in C.  Other than minor gripes like that, it's a pretty nifty package: functions as first-class objects, native arrays and maps, garbage collection, dynamic typing, and classical OOP features to boot.  However, there's a lot more to the platform than just the language, and the learning curve is quite steep.  Much like Ruby, some of the syntax is "optional" or has multiple ways to express the same thing; functions need a "value" message sent to them to be executed.  Also, most of even the basic language-level concepts are mixed in with advanced platform-specific concepts like OSC messaging, client-server internals, audio buffers, and so on, so getting your feet wet takes a lot of immersion in the concepts to understand SuperCollider's terse yet expressive syntax and impressive power.  Getting started with &lt;a href="http://processing.org/"&gt;Processing&lt;/a&gt;, which uses a much &lt;a href="http://java.com"&gt;uglier language&lt;/a&gt;, is a cakewalk by comparison.&lt;/p&gt;

&lt;p&gt;It doesn't help that the "introductory tutorial" launches first thing into a detailed description of how to use the platform to send OSC messages over the Internet, then SynthDefs, then SynthDescLibs, and then instantiating both them and their UDP equivalents, none of which, it turns out, are required to get started, when you really just want to figure out how to play a sound file from your hard disk and feed it through some reverb.  Oh no, you don't get to that until you've drudged through page after page of detailed docs, explaining each concept in depth before moving on to the next incremental step.  Memo to documentation writers: people (especially people making music) need to be able to sit down and play something after their first lesson.  You don't come home from your first piano lesson with a stack of reading on &lt;a href="http://en.wikipedia.org/wiki/Bartolomeo_Cristofori"&gt;Cristofori&lt;/a&gt; and the development of the modern soundboard, told that maybe you can start playing after you've written a 200-page treatise on the inner mechanics of the thing.  Oh sure, that's all useful to understand before too long, but to start off you want to learn just enough of a subset of the thing to feel like you can begin to be creative with it.  For an example of just how jargony the whole thing feels, I treat you to an excerpt from page 3 of the docs:&lt;/p&gt;

&lt;blockquote&gt;When notated as code in client programs, the engines have two essential parts: a name and a function. In the jargon of SuperCollider, the function is called a ugenGraphFunc.

The term ugenGraphFunc derives from the notion of a graph, which is the data structure that SuperCollider uses to organize unit generators. SuperCollider constructs the graph for you from the code it finds in your function which means that don't have to know how a graph works or that it even exists.&lt;/blockquote&gt;

&lt;p&gt;Ok, so basically it's a parse tree with a funny name.  I assumed something like that was under the hood, but if I don't have to know this, &lt;strong&gt;why the hell are you telling me on page 3 of the documentation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With that in mind, I present to you my first hard-won miniature piece of software in SuperCollider, which I wrote from scratch with scant help from the docs:&lt;/p&gt;

&lt;pre&gt;
(
 r = { |&lt;span class="Identifier"&gt;input&lt;/span&gt;, &lt;span class="Identifier"&gt;gain&lt;/span&gt;, &lt;span class="Identifier"&gt;decayTime&lt;/span&gt; = 0.5, &lt;span class="Identifier"&gt;decayFactor&lt;/span&gt; = 0.9|
 var output, buf;
 &lt;span class="Statement"&gt;if&lt;/span&gt; (gain &amp;gt; &lt;span class="Constant"&gt;0.1&lt;/span&gt;, {
         buf = &lt;span class="Type"&gt;Buffer&lt;/span&gt;.alloc(s, &lt;span class="Constant"&gt;44100&lt;/span&gt;, &lt;span class="Constant"&gt;2&lt;/span&gt;);
         output = input + r.(&lt;span class="Type"&gt;BufDelayC&lt;/span&gt;.ar(
             buf,
             input,
             decayTime,
             gain
             ), gain * decayFactor)
     }, {
         output = &lt;span class="Constant"&gt;0&lt;/span&gt;
     });
 output;
 }
)


(
 var filepath = &lt;span class="Special"&gt;&amp;quot;&lt;/span&gt;&lt;span class="Constant"&gt;remember-pans-cropped.aiff&lt;/span&gt;&lt;span class="Special"&gt;&amp;quot;&lt;/span&gt;;
 var synth = &lt;span class="Type"&gt;Buffer&lt;/span&gt;.cueSoundFile(s, filepath, &lt;span class="Constant"&gt;0&lt;/span&gt;, &lt;span class="Constant"&gt;2&lt;/span&gt;);
 {
 r.value(&lt;span class="Type"&gt;DiskIn&lt;/span&gt;.ar(&lt;span class="Constant"&gt;2&lt;/span&gt;, synth), &lt;span class="Constant"&gt;1&lt;/span&gt;, &lt;span class="Constant"&gt;0.5&lt;/span&gt;, &lt;span class="Constant"&gt;0.9&lt;/span&gt;);
 }.play;
)

&lt;/pre&gt;

&lt;p&gt;Ok, it ain't much, but it's mine and I'm proud of it.  I'll probably look at that example a few months from now and cringe knowing all the mistakes I just made, but know what?  Hell with it.  This is music, and if you leak a bit of memory here or there on your own machine before you learn how to fix it, well, that's not too steep a price for some wicked tunes.  Start with what you want, then learn just enough to get yourself there.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-5856505916606127429?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/5856505916606127429/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=5856505916606127429' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5856505916606127429'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5856505916606127429'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2009/02/cracking-supercollider.html' title='Cracking Supercollider'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-1615119457700551474</id><published>2008-08-11T19:13:00.000-07:00</published><updated>2009-02-26T00:04:57.293-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='iphone'/><title type='text'>Hitting the limits on iPhone Safari</title><content type='html'>&lt;p&gt;Hit a hard iPhone resource limit:&lt;/p&gt;

&lt;pre&gt;&lt;span class="Comment"&gt;&amp;lt;!DOCTYPE html PUBLIC &amp;quot;-//W3C//DTD XHTML 1.0 Strict//EN&amp;quot;&lt;/span&gt;
&lt;span class="Comment"&gt;&amp;quot;&lt;a href="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"&gt;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&lt;/a&gt;&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;html&lt;/span&gt;&lt;span class="Identifier"&gt; xmlns=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;&lt;a href="http://www.w3.org/1999/xhtml"&gt;http://www.w3.org/1999/xhtml&lt;/a&gt;&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;head&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;meta&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;http-equiv&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;content-type&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;content&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;text/html; charset=utf-8&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;meta&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;name&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;viewport&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;
    &lt;span class="Type"&gt;content&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; /&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;meta&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;name&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;apple-touch-fullscreen&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;content&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;YES&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; /&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;title&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;&lt;span class="Title"&gt;iPhone image loading test&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;title&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;script&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;type&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;text/javascript&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;var&lt;/span&gt;&lt;span class="Special"&gt; n = parseInt&lt;/span&gt;(&lt;span class="Constant"&gt;'02123003022000310'&lt;/span&gt;&lt;span class="Special"&gt;, &lt;/span&gt;4)&lt;span class="Special"&gt;,&lt;/span&gt;
&lt;span class="Special"&gt;    nTiles = &lt;/span&gt;0&lt;span class="Special"&gt;;&lt;/span&gt;

&lt;span class="Identifier"&gt;function&lt;/span&gt;&lt;span class="Special"&gt; drawTile&lt;/span&gt;()&lt;span class="Special"&gt; &lt;/span&gt;&lt;span class="Identifier"&gt;{&lt;/span&gt;
&lt;span class="Special"&gt;    n++;&lt;/span&gt;
&lt;span class="Special"&gt;    nTiles++;&lt;/span&gt;
&lt;span class="Special"&gt;    &lt;/span&gt;&lt;span class="Statement"&gt;document&lt;/span&gt;&lt;span class="Special"&gt;.getElementById&lt;/span&gt;(&lt;span class="Constant"&gt;'img'&lt;/span&gt;)&lt;span class="Special"&gt;.src = &lt;/span&gt;
        &lt;span class="Constant"&gt;'&lt;a href="http://h0.ortho.tiles.virtualearth.net/tiles/h0"&gt;http://h0.ortho.tiles.virtualearth.net/tiles/h0&lt;/a&gt;'&lt;/span&gt;&lt;span class="Special"&gt; +
        n.toString&lt;/span&gt;(4) &lt;span class="Special"&gt;+&lt;/span&gt; &lt;span class="Constant"&gt;'.jpeg?g=131'&lt;/span&gt;&lt;span class="Special"&gt;;&lt;/span&gt;
&lt;span class="Special"&gt;    &lt;/span&gt;&lt;span class="Statement"&gt;document&lt;/span&gt;&lt;span class="Special"&gt;.getElementById&lt;/span&gt;(&lt;span class="Constant"&gt;'count'&lt;/span&gt;)&lt;span class="Special"&gt;.innerHTML = nTiles+&lt;/span&gt;&lt;span class="Constant"&gt;''&lt;/span&gt;&lt;span class="Special"&gt;;&lt;/span&gt;

&lt;span class="Identifier"&gt;}&lt;/span&gt;

&lt;span class="Statement"&gt;window&lt;/span&gt;&lt;span class="Special"&gt;.onload = &lt;/span&gt;&lt;span class="Identifier"&gt;function&lt;/span&gt;&lt;span class="Special"&gt; &lt;/span&gt;()&lt;span class="Special"&gt; &lt;/span&gt;&lt;span class="Identifier"&gt;{&lt;/span&gt;
&lt;span class="Special"&gt;    &lt;/span&gt;&lt;span class="Statement"&gt;window&lt;/span&gt;&lt;span class="Special"&gt;.setInterval&lt;/span&gt;(&lt;span class="Special"&gt;drawTile, &lt;/span&gt;500)&lt;span class="Special"&gt;;&lt;/span&gt;
&lt;span class="Identifier"&gt;}&lt;/span&gt;&lt;span class="Special"&gt;;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;script&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;head&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;body&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;img&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;src&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;#&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;id&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;img&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;p&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;Total tiles: &lt;span class="Identifier"&gt;&amp;lt;&lt;/span&gt;&lt;span class="Statement"&gt;span&lt;/span&gt;&lt;span class="Identifier"&gt; &lt;/span&gt;&lt;span class="Type"&gt;id&lt;/span&gt;&lt;span class="Identifier"&gt;=&lt;/span&gt;&lt;span class="Constant"&gt;&amp;quot;count&amp;quot;&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="Identifier"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;span&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;p&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;body&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="Identifier"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="Statement"&gt;html&lt;/span&gt;&lt;span class="Identifier"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;

&lt;p&gt;After downloading about 210 images, the iPhone simply stops downloading new ones.  This is probably due to hitting the hard 30MB same-page resource limit as &lt;a href="http://www.alistapart.com/articles/putyourcontentinmypocketpart2"&gt;as documented on &lt;em&gt;A List Apart&lt;/em&gt;&lt;/a&gt;.  Apple itself &lt;a href="http://developer.apple.com/DOCUMENTATION/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/chapter_2_section_6.html"&gt;documents&lt;/a&gt; the limit at 2 megapixels.  Who knew that they also didn't free such resources when no longer in use?&lt;/p&gt;

&lt;p&gt;I don't see any easy way around this one, and the implications are huge: even if your app is scrupulous in conserving JavaScript and DOM memory resources, sooner or later the browser itself will fail you.  This precludes especially any browser-based Ajax mapping application, and many long-running Ajax apps in general.&lt;/p&gt;

&lt;p&gt;So much for Web 2.0 on the iPhone.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-1615119457700551474?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/1615119457700551474/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=1615119457700551474' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1615119457700551474'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1615119457700551474'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/08/iphone-resource-limits.html' title='Hitting the limits on iPhone Safari'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-5719625181758191983</id><published>2008-07-08T09:25:00.000-07:00</published><updated>2008-07-08T14:41:03.482-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='testing'/><title type='text'>Where are the JavaScript unittesting frameworks?</title><content type='html'>&lt;p&gt;Now that JavaScript has been patched together into a number of promising server-side frameworks (&lt;a href="http://peter.michaux.ca/article/7843"&gt;Synergy&lt;/a&gt;, &lt;a href="http://aptana.com/jaxer"&gt;Jaxer&lt;/a&gt;, and, coming soon(ish), &lt;a href="http://ajaxian.com/archives/rhino-on-rails-javascript-mvc-on-the-server"&gt;Rhino on Rails&lt;/a&gt;), JavaScript is really taking off as a potentially full-stack enterprise solution.&lt;/p&gt;

&lt;p&gt;One serious failing, however, seems to be the continued lack of decent developer support tools, the worst of which has got to be testing.  Sure, there are a number of decent browser test automation frameworks out there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://selenium-ide.openqa.org/"&gt;Selenium IDE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://wtr.rubyforge.org/"&gt;WATIR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.jsunit.net/"&gt;JSUnit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sure, these are all great platforms - for AUTOMATION.  Let's be clear, however: Automation is in no way the same thing as unit testing.  Automation requires a full environment, and seeks to replicate the end-user experience on the full product stack by means of emulated behaviors - mouse clicks, key presses, and so on.  Usually, such tests launch a browser app, set up the environment, execute the test case steps (which can mean multiple page loads), and then close the browser app, for EVERY TEST CASE.  Such tests are absolutely necessary for any non-trivial user-facing product.&lt;/p&gt;

&lt;p&gt;At the same time, developers need more granularity than that.  We need to test hundreds of individual methods and edge cases in our code that target specific functions.  We need a framework that can be run at build-time, and can plow through hundreds of focused test cases in a matter of seconds.  We need a browser, sans &lt;abbr title="Graphical User Interface"&gt;GUI&lt;/abbr&gt;.&lt;/p&gt;

&lt;p&gt;For the purpose of headless, command-line browser code testing, it seems only the &lt;abbr title="GNU Public License"&gt;GPL&lt;/abbr&gt; &lt;a href="http://www.thefrontside.net/crosscheck"&gt;Crosscheck framework&lt;/a&gt; offers the robustness of browser emulation required for any serious work.  Built on &lt;a href="http://www.mozilla.org/rhino/"&gt;Rhino&lt;/a&gt; with a Java layer of cleverly reverse-engineer browser behaviors, Crosscheck itself has numerous bugs and drawbacks in common browser features (no cookie emulation, poor &lt;a href="http://www.quirksmode.org/js/dom0.html"&gt;DOM Level 0&lt;/a&gt; support, nonexistent IFrames, incomplete CSS emulation), the greatest of which seems to be that &lt;a href="http://groups.google.com/group/crosscheck/browse_thread/thread/f7d4798b2af47dca"&gt;development has stalled on the project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It should be immediately apparent to any front-end developer maintaining a moderate-to-large codebase why command-line unit-testing is important.  Also, I realize that, as a developer, I should be getting things done and building solutions.  One could investigate building a command-line app for Windows (and perhaps a fork for Linux using Wine?) that would instantiate the actual browser binaries from the command line and run code without launching the browser chrome; it seems this should work at least for Internet Explorer (COM objects) and Firefox (XULRunner) and maybe Safari too.  I would like to submit patches to Crosscheck, but I'm not sure if the core developers even have time to review patches, and I don't personally have the time to sink into development of a competing framework right now.  My hope is that someone out there in the development community will take notice of this need and have the time and/or resources to tackle this problem, and maybe one day I'll be that person.  In the meantime, we'll have to settle for running our test suites in a matter of hours and not minutes.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-5719625181758191983?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/5719625181758191983/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=5719625181758191983' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5719625181758191983'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5719625181758191983'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/07/where-are-javascript-unittesting.html' title='Where are the JavaScript unittesting frameworks?'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-7204864505101231102</id><published>2008-04-15T15:04:00.000-07:00</published><updated>2009-02-26T00:05:33.403-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='history'/><title type='text'>History</title><content type='html'>&lt;pre&gt;
$ history | awk '{print $2}' | sort | uniq -c | sort -rn | head

 149 cd
 138 ls
  42 svn
  27 python
  23 vim
  18 exit
  16 locate
  11 ssh
  10 rm
   8 less
&lt;/pre&gt;

&lt;a href="http://jtauber.com/blog/2008/04/11/history/"&gt;via&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-7204864505101231102?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/7204864505101231102/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=7204864505101231102' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/7204864505101231102'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/7204864505101231102'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/04/history.html' title='History'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-2596979717381778065</id><published>2008-03-29T16:52:00.000-07:00</published><updated>2008-03-29T16:54:40.637-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='blog'/><category scheme='http://www.blogger.com/atom/ns#' term='meta'/><title type='text'>New blog</title><content type='html'>For all 2.3 of you out there reading this, I'd like to announce a &lt;a href="http://blog.davidgolightly.net"&gt;new blog on "metaphysics?"&lt;/a&gt; so I can keep a clean sterile separation between work and life.  My postings on this blog will continue to be exactly as infrequent as they have been since I started.

Thank you.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-2596979717381778065?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/2596979717381778065/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=2596979717381778065' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/2596979717381778065'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/2596979717381778065'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/03/new-blog.html' title='New blog'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-7917793661406361094</id><published>2008-02-27T20:54:00.001-08:00</published><updated>2008-02-27T20:54:25.276-08:00</updated><title type='text'>Why Man Creates</title><content type='html'>&lt;object width="425" height="355"&gt;&lt;param name="movie" value="http://www.youtube.com/v/penl-HYfMCg"&gt;&lt;/param&gt;&lt;param name="wmode" value="transparent"&gt;&lt;/param&gt;&lt;embed src="http://www.youtube.com/v/penl-HYfMCg" type="application/x-shockwave-flash" wmode="transparent" width="425" height="355"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-7917793661406361094?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/7917793661406361094/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=7917793661406361094' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/7917793661406361094'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/7917793661406361094'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/02/why-man-creates.html' title='Why Man Creates'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-2998514368706575690</id><published>2008-02-09T11:50:00.000-08:00</published><updated>2008-02-09T12:44:39.743-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='economics'/><category scheme='http://www.blogger.com/atom/ns#' term='internet'/><title type='text'>Strategies for Success (and other myths)</title><content type='html'>&lt;p&gt;The writings and thoughts of Karl Marx and Friedrich Engels and orthodox Communism in general date themselves squarely between the Industrial Revolution and Darwin's historic Origins.  The revolutionary implementation of Communism was doomed to failure, of course, because by the time it gained critical mass it was already obsolete; groups of humans can't be organized as machines are organized.  Top-down mandates of economic equality don't work, because the system will always be more complex than even the most tyrannical police state can enforce.  People under duress may provide acceptable manual labor but don't contribute new ideas or new efficiencies to the process when their main concern is not personal investment in work but in following orders; thus micromanaged economies will always underperform freemarket systems.&lt;/p&gt;

&lt;p&gt;The lesson of Communism that especially American right-wing capitalists don't want to learn is that micromanagement and monopoly are anathema to the principles of the free market.  Applying the principles of Darwinian natural selection to the economic domain, we see that a healthy economic ecosystem is one in which no single player is too dominant (see: &lt;a href="http://www.livescience.com/animals/060718_big_animals.html"&gt;super predators&lt;/a&gt;), but rather neck-and-neck competition keeps everyone on their toes while allowing smaller players to emerge as competitive with more established ones.  Conversely, an economy of monoculture tends toward stagnation.  Companies that grow too large become entrenched in a single business model and risk the ire of large partners when proposing innovations in production methods (see: Microsoft and IE).&lt;/p&gt;

&lt;p&gt;Government clearly has a role to play in keeping the market competitive by regulating and sometimes smashing large corporations into smaller pieces.  But more importantly, ownership of past success is not a guarantor for future success.  Top-down institutionalization of past success models is a sure sign that the business is failing to see the next step in its evolution.  In this regard, corporate (read: Stalinist) ownership of information and ideas will always fail in the face of bottom-up Darwinian trial-and-error.  This is especially true in industries touched by technology, and we're already seeing established media companies, record companies, and television networks collapse under their own weight.&lt;/p&gt;

&lt;p&gt;Enter the Internet.  To the extent that it remains free, equal, and open, it provides the most fertile soil yet for innovation and progress (of ideas, of creativity, of economic advancement, of democracy).  The chaotic hordes of the MySpace/YouTube generation will surely hasten the extinction of the giant record companies and the television networks, as &lt;a href="http://en.wikipedia.org/wiki/The_Wisdom_of_Crowds"&gt;the wisdom of crowds&lt;/a&gt; will do a better job determining what the crowds like than any closed, proprietary group of mavens in a boardroom or a laboratory ever will.&lt;/p&gt;

&lt;p&gt;The lesson for internet companies trying to attract millions?  &lt;a href="http://www.paulgraham.com/startuplessons.html"&gt;Release early and often&lt;/a&gt;.  Keep your &lt;a href="http://en.wikipedia.org/wiki/Loose_coupling"&gt;coupling loose&lt;/a&gt;.  Submit to what your users are telling you - investing 10 months in a project you're sure will be a hit is a huge risk; you'd better be basing your decisions on reams of user feedback and research, because your perfect vision for the product will always fail you unless you're playing with rules that have been subjected to the Darwinian rigor of millions of little trial-and-error tests.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-2998514368706575690?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/2998514368706575690/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=2998514368706575690' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/2998514368706575690'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/2998514368706575690'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/02/strategies-for-success-and-other-myths.html' title='Strategies for Success (and other myths)'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-5835952409521962245</id><published>2008-02-02T09:14:00.000-08:00</published><updated>2009-03-04T13:06:22.430-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='date formatter'/><title type='text'>Easy Date Formatter</title><content type='html'>This should do the trick for most uses:


&lt;pre&gt;
&lt;span class="Comment"&gt;/** &lt;/span&gt;
&lt;span class="Comment"&gt; * takes a string in form 'd/m/Y'&lt;/span&gt;
&lt;span class="Comment"&gt; * &lt;/span&gt;
&lt;span class="Comment"&gt; * d =&amp;gt; day (2 digits, leading 0)&lt;/span&gt;
&lt;span class="Comment"&gt; * D =&amp;gt; day (2 digits, no leading)&lt;/span&gt;
&lt;span class="Comment"&gt; * l =&amp;gt; day (of week, text, 3 chars)&lt;/span&gt;
&lt;span class="Comment"&gt; * L =&amp;gt; day (of week, text, full length)&lt;/span&gt;
&lt;span class="Comment"&gt; * &lt;/span&gt;
&lt;span class="Comment"&gt; * m =&amp;gt; month (2 digits, leading 0)&lt;/span&gt;
&lt;span class="Comment"&gt; * M =&amp;gt; month (2 digits, no leading 0)&lt;/span&gt;
&lt;span class="Comment"&gt; * f =&amp;gt; month (text, 3 chars)&lt;/span&gt;
&lt;span class="Comment"&gt; * F =&amp;gt; month (text, full length)&lt;/span&gt;
&lt;span class="Comment"&gt; * &lt;/span&gt;
&lt;span class="Comment"&gt; * y =&amp;gt; year (2 digits)&lt;/span&gt;
&lt;span class="Comment"&gt; * Y =&amp;gt; year (4 digits)&lt;/span&gt;
&lt;span class="Comment"&gt; * &lt;/span&gt;
&lt;span class="Comment"&gt; * g =&amp;gt; hour (12hr, leading 0)&lt;/span&gt;
&lt;span class="Comment"&gt; * G =&amp;gt; hour (12hr, no leading)&lt;/span&gt;
&lt;span class="Comment"&gt; * i =&amp;gt; minute&lt;/span&gt;
&lt;span class="Comment"&gt; * s =&amp;gt; second&lt;/span&gt;
&lt;span class="Comment"&gt; * t =&amp;gt; hour (12hr, formatted with am/pm)&lt;/span&gt;
&lt;span class="Comment"&gt; * &lt;/span&gt;
&lt;span class="Comment"&gt; * @param {String} format String for the formatting of this Date object&lt;/span&gt;
&lt;span class="Comment"&gt; * @return {String}&lt;/span&gt;
&lt;span class="Comment"&gt; */&lt;/span&gt;
&lt;span class="Type"&gt;Date&lt;/span&gt;.prototype.format = (&lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt;
    &lt;span class="Identifier"&gt;var&lt;/span&gt; days = &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'Sun'&lt;/span&gt;, &lt;span class="Constant"&gt;'Mon'&lt;/span&gt;, &lt;span class="Constant"&gt;'Tue'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'Wed'&lt;/span&gt;, &lt;span class="Constant"&gt;'Thu'&lt;/span&gt;, &lt;span class="Constant"&gt;'Fri'&lt;/span&gt;, &lt;span class="Constant"&gt;'Sat'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;;
    &lt;span class="Identifier"&gt;var&lt;/span&gt; fulldays = &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'Sunday'&lt;/span&gt;, &lt;span class="Constant"&gt;'Monday'&lt;/span&gt;, &lt;span class="Constant"&gt;'Tuesday'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'Wednesday'&lt;/span&gt;, &lt;span class="Constant"&gt;'Thursday'&lt;/span&gt;, &lt;span class="Constant"&gt;'Friday'&lt;/span&gt;, &lt;span class="Constant"&gt;'Saturday'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;;
    &lt;span class="Identifier"&gt;var&lt;/span&gt; months = &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'Jan'&lt;/span&gt;, &lt;span class="Constant"&gt;'Feb'&lt;/span&gt;, &lt;span class="Constant"&gt;'Mar'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'Apr'&lt;/span&gt;, &lt;span class="Constant"&gt;'May'&lt;/span&gt;, &lt;span class="Constant"&gt;'Jun'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'Jul'&lt;/span&gt;, &lt;span class="Constant"&gt;'Aug'&lt;/span&gt;, &lt;span class="Constant"&gt;'Sep'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'Oct'&lt;/span&gt;, &lt;span class="Constant"&gt;'Nov'&lt;/span&gt;, &lt;span class="Constant"&gt;'Dec'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;;
    &lt;span class="Identifier"&gt;var&lt;/span&gt; fullmonths = &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'January'&lt;/span&gt;, &lt;span class="Constant"&gt;'February'&lt;/span&gt;, &lt;span class="Constant"&gt;'March'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'April'&lt;/span&gt;, &lt;span class="Constant"&gt;'May'&lt;/span&gt;, &lt;span class="Constant"&gt;'June'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'July'&lt;/span&gt;, &lt;span class="Constant"&gt;'August'&lt;/span&gt;, &lt;span class="Constant"&gt;'September'&lt;/span&gt;,
        &lt;span class="Constant"&gt;'October'&lt;/span&gt;, &lt;span class="Constant"&gt;'November'&lt;/span&gt;, &lt;span class="Constant"&gt;'December'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;;

    &lt;span class="Identifier"&gt;function&lt;/span&gt; twoDigits(n) &lt;span class="Identifier"&gt;{&lt;/span&gt;
        &lt;span class="Statement"&gt;return&lt;/span&gt; (n&amp;lt;10? &lt;span class="Constant"&gt;'0'&lt;/span&gt;:&lt;span class="Constant"&gt;''&lt;/span&gt;)+n;
    &lt;span class="Identifier"&gt;}&lt;/span&gt;

    &lt;span class="Statement"&gt;return&lt;/span&gt; &lt;span class="Identifier"&gt;function&lt;/span&gt; formatDate(format) &lt;span class="Identifier"&gt;{&lt;/span&gt;
        &lt;span class="Identifier"&gt;var&lt;/span&gt; d = &lt;span class="Identifier"&gt;this&lt;/span&gt;;

        &lt;span class="Identifier"&gt;var&lt;/span&gt; matches = &lt;span class="Identifier"&gt;this&lt;/span&gt;.matches || &lt;span class="Identifier"&gt;{&lt;/span&gt;
            &lt;span class="Constant"&gt;'d'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; twoDigits(d.getDate()); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'D'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; d.getDate()+&lt;span class="Constant"&gt;''&lt;/span&gt;; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'l'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; days&lt;span class="Identifier"&gt;[&lt;/span&gt;d.getDay()&lt;span class="Identifier"&gt;]&lt;/span&gt;; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'L'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; fulldays&lt;span class="Identifier"&gt;[&lt;/span&gt;d.getDay()&lt;span class="Identifier"&gt;]&lt;/span&gt;; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'m'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; twoDigits(d.getMonth()+1); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'M'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; d.getMonth()+1; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'f'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; months&lt;span class="Identifier"&gt;[&lt;/span&gt;d.getMonth()&lt;span class="Identifier"&gt;]&lt;/span&gt;; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'F'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; fullmonths&lt;span class="Identifier"&gt;[&lt;/span&gt;d.getMonth()&lt;span class="Identifier"&gt;]&lt;/span&gt;; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'y'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; (d.getFullYear()+&lt;span class="Constant"&gt;''&lt;/span&gt;).slice(2, 4); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'Y'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; (d.getFullYear()+&lt;span class="Constant"&gt;''&lt;/span&gt;); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'g'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; twoDigits(d.getHours()%12); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'G'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; d.getHours()%12+&lt;span class="Constant"&gt;''&lt;/span&gt;; &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'i'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; twoDigits(d.getMinutes()); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'s'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; twoDigits(d.getSeconds()); &lt;span class="Identifier"&gt;}&lt;/span&gt;,
                &lt;span class="Constant"&gt;'t'&lt;/span&gt;: &lt;span class="Identifier"&gt;function&lt;/span&gt; () &lt;span class="Identifier"&gt;{&lt;/span&gt; &lt;span class="Statement"&gt;return&lt;/span&gt; (d.getHours() &amp;gt; 11? &lt;span class="Constant"&gt;'am'&lt;/span&gt; : &lt;span class="Constant"&gt;'pm'&lt;/span&gt;); &lt;span class="Identifier"&gt;}&lt;/span&gt;
        &lt;span class="Identifier"&gt;}&lt;/span&gt;;
        &lt;span class="Identifier"&gt;this&lt;/span&gt;.matches = matches;

        &lt;span class="Statement"&gt;return&lt;/span&gt; format.replace(&lt;span class="Constant"&gt;/[dDlLmMfFyYgGist]/g&lt;/span&gt;, &lt;span class="Identifier"&gt;function&lt;/span&gt; (match) &lt;span class="Identifier"&gt;{&lt;/span&gt;
            &lt;span class="Statement"&gt;return&lt;/span&gt; matches&lt;span class="Identifier"&gt;[&lt;/span&gt;match&lt;span class="Identifier"&gt;]&lt;/span&gt;();
        &lt;span class="Identifier"&gt;}&lt;/span&gt;);
    &lt;span class="Identifier"&gt;}&lt;/span&gt;;
&lt;span class="Identifier"&gt;}&lt;/span&gt;)();

&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-5835952409521962245?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/5835952409521962245/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=5835952409521962245' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5835952409521962245'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5835952409521962245'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/02/easy-date-formatter.html' title='Easy Date Formatter'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-5292128630035123453</id><published>2008-01-25T12:55:00.000-08:00</published><updated>2009-03-04T13:02:26.337-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='cssx'/><category scheme='http://www.blogger.com/atom/ns#' term='compiler'/><category scheme='http://www.blogger.com/atom/ns#' term='css'/><title type='text'>Introducing CSSx</title><content type='html'>&lt;p&gt;Sick of the lack of syntactic sugar for CSS?  While many designers are fine with the simplicity of CSS, some developers managing large codebases (such as Zillow's still-terse 2500-line master.css) have been flailing for sanity.  Enter CSSx, a syntax-enhancing compiler for CSS.  It accepts all currently-valid CSS syntax, so you can use your current CSS chops unaltered, but adds variables and block nesting to the language.&lt;/p&gt;

&lt;pre&gt;/* sample.cssx */

$red = #f00 /* a comment here*/;
$block = {
    div {
        padding: 10px;
    }
    color: $red;
    border-width: 1px;
    margin: 20px 10px 15px 0;
}

.warning {
    color: $red;
}

.content {
    .warning {
        width: 200px;
    }

    cite { @rule($block); }

    p {
        span.tooltip {
            color: blue;
        }
        font-size: 1.1em;
    }

    .header {
        @rule($block);
        font-size: 0.8em;
    }

    font-family: Verdana, Arial, sans-serif;
}
&lt;/pre&gt;

&lt;p&gt;It's still very beta (I'd peg it at 0.1), but please download, give it a spin, and give me feedback on what works/doesn't work for you, or what you'd like to see added.  &lt;a href="http://code.google.com/p/cssx/"&gt;Download CSSx from Google Code.&lt;/a&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-5292128630035123453?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/5292128630035123453/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=5292128630035123453' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5292128630035123453'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/5292128630035123453'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2008/01/introducing-cssx.html' title='Introducing CSSx'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-4080787444111044866</id><published>2007-10-13T19:04:00.000-07:00</published><updated>2009-03-04T13:03:15.764-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='ecmascript'/><category scheme='http://www.blogger.com/atom/ns#' term='syntax'/><category scheme='http://www.blogger.com/atom/ns#' term='second-guessing'/><title type='text'>Type-Converting Operator Considered Ridiculous</title><content type='html'>&lt;p&gt;I have come to question the wisdom of the type-converting &lt;tt&gt;==&lt;/tt&gt; operator in JavaScript.  The algorithm (defined in &amp;sect;11.9.3 of the &lt;a href="http://www.ecma-international.org/publications/standards/Ecma-262.htm"&gt;ECMA-262 v3 spec&lt;/a&gt;) requires no fewer than 22 steps to execute, and produces such amazing intrasitivities as:&lt;/p&gt;

&lt;pre&gt;
&amp;gt;&amp;gt;&amp;gt; '0' == 0
true

&amp;gt;&amp;gt;&amp;gt; 0 == ''
true

&amp;gt;&amp;gt;&amp;gt; '' == '0'
false
&lt;/pre&gt;

&lt;p&gt;and random quirkiness such as:&lt;/p&gt;

&lt;pre&gt;
&amp;gt;&amp;gt;&amp;gt; ' \t\r\n' == 0
true
&lt;/pre&gt;

&lt;p&gt;but:&lt;/p&gt;

&lt;pre&gt;
&amp;gt;&amp;gt;&amp;gt; null == 0
false

&amp;gt;&amp;gt;&amp;gt; null == false
false

&amp;gt;&amp;gt;&amp;gt; null == ''
false

&amp;gt;&amp;gt;&amp;gt; null == undefined
true
&lt;/pre&gt;

&lt;p&gt;Do these behaviors strike anyone else as, well, a bit off?  In the first example (discussed &lt;a href="http://tobielangel.com/2007/6/16/javascript-quirks"&gt;elsewhere&lt;/a&gt;) the intransitivity of the equality operator is a result of wanting an equality operator to play well within a weakly-typed language.  If you want to be able to compare numbers to their string representations as though they were of the same type, while still preserving string comparison, this is the logical result.  The second example is a result of the ToNumber algorithm, described in &amp;sect;9.3ff.  The whitespace conversion to number value &lt;tt&gt;0&lt;/tt&gt; is strange, but defined in the spec (in fact, it's a result of the same algorithm that dictates &lt;tt&gt;Number(' ') =&gt; 0&lt;/tt&gt;).  The third example seems bizarre as well; I'm not sure why null would be treated differently than &lt;tt&gt;0&lt;/tt&gt; or &lt;tt&gt;false&lt;/tt&gt;, but the same as &lt;tt&gt;undefined&lt;/tt&gt;, in such expressions.  &lt;a href="http://developer.mozilla.org/en/docs/A_re-introduction_to_JavaScript#Other_types"&gt;Many&lt;/a&gt; articles from otherwise &lt;a href="http://javascript.crockford.com/style2.html"&gt;reputable sources&lt;/a&gt; often describe all of these values in terms of "truthiness" and "falsiness", but these articles sometimes gloss over these distinctions.&lt;/p&gt;

&lt;p&gt;I don't condone transitioning JS into a strongly-typed system, but some of these behaviors are quite subtle.  Given the fact that the vast majority of JavaScript programmers seem to be unaware of the strict-equality operator &lt;tt&gt;===&lt;/tt&gt; (and its negative counterpart, &lt;tt&gt;!==&lt;/tt&gt;), it's no wonder behaviors such as these can cause confusion.  See also &lt;a href="http://www.isolani.co.uk/blog/javascript/TruthyFalsyAndTypeCasting"&gt;this article&lt;/a&gt; for even more pitfalls, this time simply with math operators combining string and numeric types.&lt;/p&gt;

&lt;p&gt;So as &lt;a href="http://javascript.crockford.com/code.html"&gt;Crockford says&lt;/a&gt;, use the strict comparison operator &lt;tt&gt;===&lt;/tt&gt; unless you know what you're doing.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-4080787444111044866?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/4080787444111044866/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=4080787444111044866' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4080787444111044866'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4080787444111044866'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/10/type-converting-operator-considered.html' title='Type-Converting Operator Considered Ridiculous'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-4363230353061566928</id><published>2007-10-04T12:43:00.000-07:00</published><updated>2009-03-04T13:10:14.184-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='cryptography'/><category scheme='http://www.blogger.com/atom/ns#' term='security'/><category scheme='http://www.blogger.com/atom/ns#' term='satire'/><category scheme='http://www.blogger.com/atom/ns#' term='compression'/><title type='text'>All the Code Security You Need</title><content type='html'>&lt;p&gt;Lots of beginning JS coders seem to have this idea that the web is all about having your cake and eating it too.  I want to participate in the glorious Internet revolution, I just don't want to share my ideas or information with anybody else.  I'm writing rich internet applications that are so great, just so unique, that I want to make sure nobody else can possibly look at my code and steal all those great juicy super-awesome proprietary ideas I've got.  And maybe, just for bragging rights, I want my files to look HUGE as well.  I've mocked up a handy little device that can take care of both tasks at once.  Behold, the decompressJS module:&lt;/p&gt;

&lt;pre&gt;
var decompressJS = (function () {
    var encoding = [' ', '\t', '\n', '\r'];
    var revEncoding = (function () {
        var ec = {};
        for (var i=0; i&amp;lt;encoding.length; i++) {
            ec[encoding[i]] = i;
        };
        return ec;
    })();

    return {
        encode: function (codeText) {
            var out = [], cc, i=0, len=codeText.length;

            while (i&amp;lt;len) {
                cc = codeText.charCodeAt(i++);

                // JS chars are actually 16-bit UTF-16 chars, 
                // so we need 4 chars encoded per source char
                // to fully encode
                for (var j=0; j&amp;lt;8; j+=2) {
                    out.push(encoding[(cc&gt;&gt;j)&amp;3]);
                }

            }
            return out.join('');
        },

        decode: function (encodedText) {
            var out = [], cur, i=0, len=encodedText.length;
            while (i&amp;lt;len) {
                cur = 0;
                for (var j=0; j&amp;lt;8; j+=2) {
                    cur += revEncoding[encodedText.charAt(i++)]&amp;lt;&amp;lt;j;
                }
                out.push(String.fromCharCode(cur));
            }
            return out.join('');
        }
    };

})();
&lt;/pre&gt;

&lt;p&gt;Like any mature programming paradigm, my decompressJS can even effectively &lt;em&gt;encode&lt;/em&gt; and &lt;em&gt;decode&lt;/em&gt; itself, the above 1225-character-long code example (including whitespace) decompressed neatly into an all-whitespace string with no fewer than 4900 characters!  Just run your code through this baby and it'll come out the other end fully converted into only invisible whitespace characters!  Transfer your JS code over the wire, making it look like nothing but an empty file with all whitespace!  Magically increase your file size by a factor of 4!  Impress your colleagues with your invisible code!  Who's laughing now, code stealers?&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-4363230353061566928?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/4363230353061566928/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=4363230353061566928' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4363230353061566928'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4363230353061566928'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/10/all-code-security-you-need.html' title='All the Code Security You Need'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-4663809726079689338</id><published>2007-07-27T17:47:00.000-07:00</published><updated>2007-07-27T17:51:05.904-07:00</updated><title type='text'>Glorious links</title><content type='html'>&lt;p&gt;BoingBoing's been on a good tear recently.  Better than usual.  For those of you whose eyes have glazed over, go back and re-read these posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://feeds.feedburner.com/~r/boingboing/iBag/~3/137376915/religious_priming_pr.html"&gt;Religious priming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://feeds.feedburner.com/~r/boingboing/iBag/~3/137387054/musurgia_universalis.html"&gt;Musurgia Universalis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://feeds.feedburner.com/~r/boingboing/iBag/~3/135678113/kevin_kelly_the_tech.html"&gt;Technology and the Meaning of Life&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-4663809726079689338?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/4663809726079689338/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=4663809726079689338' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4663809726079689338'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4663809726079689338'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/07/glorious-links.html' title='Glorious links'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-4509013573221764119</id><published>2007-07-17T21:52:00.000-07:00</published><updated>2009-03-04T13:11:48.371-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='JSON'/><category scheme='http://www.blogger.com/atom/ns#' term='templating'/><category scheme='http://www.blogger.com/atom/ns#' term='DOM'/><category scheme='http://www.blogger.com/atom/ns#' term='DRY'/><title type='text'>All the Templating You Need</title><content type='html'>&lt;pre&gt;
&lt;span class="Identifier"&gt;function&lt;/span&gt; replicate(items, template, defaults) &lt;span class="Identifier"&gt;{&lt;/span&gt;
    &lt;span class="Identifier"&gt;var&lt;/span&gt; indices = &lt;span class="Identifier"&gt;{}&lt;/span&gt;;
    &lt;span class="Identifier"&gt;var&lt;/span&gt; i=0, item;

    &lt;span class="Comment"&gt;// build direct map of column names -&amp;gt; row index&lt;/span&gt;
    &lt;span class="Statement"&gt;while&lt;/span&gt; ((item = data.columns&lt;span class="Identifier"&gt;[&lt;/span&gt;i&lt;span class="Identifier"&gt;]&lt;/span&gt;)) indices&lt;span class="Identifier"&gt;[&lt;/span&gt;item&lt;span class="Identifier"&gt;]&lt;/span&gt; = i++;

    &lt;span class="Statement"&gt;return&lt;/span&gt; data.rows.map(&lt;span class="Identifier"&gt;function&lt;/span&gt; (row) &lt;span class="Identifier"&gt;{&lt;/span&gt;
            &lt;span class="Statement"&gt;return&lt;/span&gt; template.replace(&lt;span class="Constant"&gt;/__\$\{(.+?)\}__/g&lt;/span&gt;, &lt;span class="Identifier"&gt;function&lt;/span&gt; (str, keyword) &lt;span class="Identifier"&gt;{&lt;/span&gt;
                &lt;span class="Statement"&gt;return&lt;/span&gt; (keyword &lt;span class="Statement"&gt;in&lt;/span&gt; indices)? row&lt;span class="Identifier"&gt;[&lt;/span&gt;indices&lt;span class="Identifier"&gt;[&lt;/span&gt;keyword&lt;span class="Identifier"&gt;]]&lt;/span&gt; : &lt;span class="Constant"&gt;''&lt;/span&gt;;
                &lt;span class="Identifier"&gt;}&lt;/span&gt;);
            &lt;span class="Identifier"&gt;}&lt;/span&gt;).join(&lt;span class="Special"&gt;'\n'&lt;/span&gt;);
&lt;span class="Identifier"&gt;}&lt;/span&gt;

&lt;span class="Identifier"&gt;var&lt;/span&gt; data = &lt;span class="Identifier"&gt;{&lt;/span&gt;
    &lt;span class="Constant"&gt;'columns'&lt;/span&gt;: &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'adj'&lt;/span&gt;, &lt;span class="Constant"&gt;'noun'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;,
    &lt;span class="Constant"&gt;'rows'&lt;/span&gt;: &lt;span class="Identifier"&gt;[&lt;/span&gt;
        &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'main'&lt;/span&gt;, &lt;span class="Constant"&gt;'man'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;,
    &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'leading'&lt;/span&gt;, &lt;span class="Constant"&gt;'lady'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;,
    &lt;span class="Identifier"&gt;[&lt;/span&gt;&lt;span class="Constant"&gt;'green'&lt;/span&gt;, &lt;span class="Constant"&gt;'dreams'&lt;/span&gt;&lt;span class="Identifier"&gt;]&lt;/span&gt;
        &lt;span class="Identifier"&gt;]&lt;/span&gt;
&lt;span class="Identifier"&gt;}&lt;/span&gt;;

&lt;span class="Identifier"&gt;var&lt;/span&gt; template = &lt;span class="Constant"&gt;'&amp;lt;p&amp;gt;__${adj}__ __${noun}__&amp;lt;/p&amp;gt;'&lt;/span&gt;;
replicate(data, template);


&lt;/pre&gt;

&lt;p&gt;&lt;a href="http://peter.michaux.ca/article/2652"&gt;Peter Michaux&lt;/a&gt; has some nice ideas about keeping the &lt;a href="http://www.json.org"&gt;JSON&lt;/a&gt; format &lt;a href="http://c2.com/cgi/wiki?DontRepeatYourself"&gt;DRY&lt;/a&gt;, so that data returned resembles something more like a list of &lt;a href="http://jtauber.com/blog/2006/04/15/python_tuples_are_not_just_constant_lists"&gt;Python tuples&lt;/a&gt;.  (Python is also probably the single language that helped me to understand efficient JavaScript patterns.)&lt;/p&gt;

&lt;p&gt;Client-side transforms - converting an XML or JSON response into HTML on the client, to save server bandwidth and processing time - are a key part of modern web apps, but I'm not sure about a transform system that implements full-blown JavaScript logic.  Branching or looping can be implemented easily in transforming functions; several templates can be used and plugged in to each other, leading to nested data structures in the response.  (Hopefully, time permitting, I'll get to demonstrate how that works soon.)&lt;/p&gt;

&lt;p&gt;innerHTML may not be a part of any standard, but there's no reason why it shouldn't be.  Sometimes we need to interact with the DOM as a tree, sometimes it's more useful to unleash JavaScript's string parsing and regex power on it.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-4509013573221764119?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/4509013573221764119/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=4509013573221764119' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4509013573221764119'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/4509013573221764119'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/07/all-templating-you-need.html' title='All the Templating You Need'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-1261995055888376861</id><published>2007-07-09T13:48:00.000-07:00</published><updated>2009-03-04T13:12:35.161-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='minification'/><category scheme='http://www.blogger.com/atom/ns#' term='shell script'/><category scheme='http://www.blogger.com/atom/ns#' term='css'/><title type='text'>One-line CSS minifier</title><content type='html'>&lt;p&gt;CSS minification in one line:&lt;/p&gt;

&lt;pre&gt;$ cat sourcefile.css | sed -e 's/^[ \t]*//g; s/[ \t]*$//g; s/\([:{;,]\) /\1/g; s/ {/{/g; s/\/\*.*\*\///g; /^$/d' | sed -e :a -e '$!N; s/\n\(.\)/\1/; ta' &amp;gt;target.css&lt;/pre&gt;

&lt;p&gt;With comments:&lt;/p&gt;

&lt;pre&gt;$ cat sourcefile.css | sed -e '
s/^[ \t]*//g;         # remove leading space
s/[ \t]*$//g;         # remove trailing space
s/\([:{;,]\) /\1/g;   # remove space after a colon, brace, semicolon, or comma
s/ {/{/g;             # remove space before a semicolon
s/\/\*.*\*\///g;      # remove comments
/^$/d                 # remove blank lines
' | sed -e :a -e '$!N; s/\n\(.\)/\1/; ta # remove all newlines
' &amp;gt; target.css
&lt;/pre&gt;

&lt;p&gt;Using this script, I was able to chop about 29% (10K) off our master.css file.  Assumes lines end in semicolons that should end in semicolons.  May not play well with certain freakish outdated CSS hacks.  Use at your own risk, and always test throughly before releasing into the wild.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-1261995055888376861?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/1261995055888376861/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=1261995055888376861' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1261995055888376861'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1261995055888376861'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/07/one-line-css-minifier.html' title='One-line CSS minifier'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-3208958496035669129</id><published>2007-07-07T17:11:00.000-07:00</published><updated>2007-07-08T15:43:38.243-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='css queries'/><category scheme='http://www.blogger.com/atom/ns#' term='css'/><title type='text'>The Problem with SlickSpeed</title><content type='html'>&lt;p&gt;For the past month or so, there's been a lot of noise about the &lt;a href="http://mootools.net/slickspeed/"&gt;SlickSpeed Selectors Test Suite&lt;/a&gt;.  Since I'm in the market for a good selector engine for Zillow, and since it's a bit of a rite of passage (a front-end web dev's equivalent of &lt;a href="http://steve-yegge.blogspot.com/2007/06/rich-programmer-food.html"&gt;compiler authoring&lt;/a&gt;?), I wrote my own, to see how well I could do and to see how it stacks up to the rest.&lt;/p&gt;
&lt;p&gt;So of course, I modified the suite (under its MIT license) to test my little attempt as well.  I was pleased with my initial results, but found the test document that comes packaged with the suite to be a little simplistic.  Not enough variety or depth of nesting; the resulting DOM structures don't really resemble what I look at on a daily basis at work.  I wanted to measure performance in the wild.  So I replaced Shakespeare's As You Like It with the Home Details Page for Zillow.com, perhaps the most complex page on the site.  Among other things, it includes a photo gallery, a Flash map, an Live Local-based Bird's Eye View map, a chart widget, several ads, tables, etc.&lt;/p&gt;
&lt;p&gt;The results, you can see for yourself, &lt;a href="http://dev.davidgolightly.net/SlickSpeed%20Selectors%20Test%20for%20Zillow.htm"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As it turns out, according to SlickSpeed, my engine outperforms all but 2 of the other engines on Firefox, and is the best performer on IE7.&lt;/p&gt;
&lt;p&gt;So my misgivings on the nature of the document wandered over to the construction of the queries.  The given queries perform a "breadth" pass, but they don't really provide a "depth" pass including all manner of combinations of possible selectors, so I wrote my own addition to the suite that picks random elements from the DOM and generates a matching CSS1 selector for it.  You can see the dynamic test suite &lt;a href="http://dev.davidgolightly.net/Dynamic%20SlickSpeed%20Selectors%20Test%20for%20Zillow.htm"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, my Element.select engine's performance is fair to decent at best, but no longer the front-runner.  Unless I can iron out the kinks, I might look into Ext's engine, especially since it fits nicely into the &lt;a href="http://developer.yahoo.com/yui/"&gt;Yahoo! UI library&lt;/a&gt; we use at Zillow.&lt;/p&gt;
&lt;p&gt;On the other hand, my Element.select engine is stand-alone and does not provide any other services or dependencies.  It's a whopping 6KB (minified), but I wouldn't recommend the use of a CSS query engine for anything short of a full-scale web application anyway.&lt;/p&gt;
&lt;p&gt;Some thoughts, though:  For reasons that should be self-explanatory, it appears that all of the CSS engine makers are optimizing for Firefox.  And once again, Opera's JavaScript engine (named Linear B) and DOM implementation beats out all the rest. Performance on IE looks to be all-around poorer.  The Internet Explorer Team certainly has their work cut out for them, not only in improving their DOM and JScript performance and their developer tools (a decent profiler and a debugger that's not attached to Visual Studio would be nice), but also in winning over a hostile developer community.  I guess that's what happens when the maker of the World's Number One Browser shuts down their development team for 5 years.&lt;/p&gt;
&lt;p&gt;Prototype and MooTools appear to be compiling the CSS selectors into XPath statements for Firefox's (and Opera's) built-in xpath evaluator (too bad IE forgot to allow MSXML's XPath engine to apply to the HTML DOM).  While the DOM performance for these XPath-based implementations is fantastic, they also help underline the end-user experience difference between browsers.  Let's hope users take notice how much faster the leading non-IE browsers are in comparison; it's hard to win users over on the basis of standards compliance alone.&lt;/p&gt;
&lt;p&gt;If nothing else, I hope my modified SlickSpeeds will help CSS query engine developers focus on what's important: CSS1 queries.  The time scores at the bottom of the SlickSpeed test skew heavily toward obscure pseudoclass and attribute selectors which I for one won't use most of the time.  It's the meat-and-potatoes tag, class, and ID that really count.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-3208958496035669129?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/3208958496035669129/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=3208958496035669129' title='4 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/3208958496035669129'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/3208958496035669129'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/07/problem-with-slickspeed.html' title='The Problem with SlickSpeed'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-3681843453565990276</id><published>2007-07-01T23:59:00.000-07:00</published><updated>2007-07-02T00:22:38.148-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='google'/><title type='text'>Sicko</title><content type='html'>&lt;p&gt;As we all know, Google has great power.  It's power that comes from the masses: utilizing and channeling the activities, ideas and opinions of millions via the Web.  All that information and trust capital can be a powerful tool for sustaining an environment that encourages democracy.  &lt;a href="http://google-health-ads.blogspot.com/2007/06/does-negative-press-make-you-sicko.html"&gt;Or not.&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Update:&lt;/h4&gt;
&lt;p&gt;Apparently now, for some at Google, &lt;a href="http://google-health-ads.blogspot.com/2007/07/my-opinion-and-googles.html"&gt;democracy is available to the highest bidder.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;But the more important point, since I doubt that too many people care about my personal opinion, is that advertising is an effective medium for handling challenges that a company or industry might have. You could even argue that it's especially appropriate for a public policy issue like healthcare. Whether the healthcare industry wants to rebut charges in Mr. Moore's movie, or whether Mr. Moore wants to challenge the healthcare industry, advertising is a very democratic and effective way to participate in a public dialogue.&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;em&gt;That is Google's opinion....&lt;/em&gt;&lt;/blockquote&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-3681843453565990276?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/3681843453565990276/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=3681843453565990276' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/3681843453565990276'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/3681843453565990276'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/07/sicko.html' title='Sicko'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-7489586816823885344</id><published>2007-07-01T18:54:00.000-07:00</published><updated>2007-07-01T20:19:23.026-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='usability'/><title type='text'>Web-Native UX</title><content type='html'>&lt;p&gt;I'd like to address something many in the User Experience community would rather avoid, since many times it may interfere with deploying the latest cool widget or Ajax technique that comes down the pike.  I want to talk about User Experience Consistency and the web.  Because while standards bodies have come into being to coordinate the development of the &lt;a href="http://www.whatwg.org/specs/web-apps/current-work/"&gt;cornerstone&lt;/a&gt; &lt;a href="http://www.w3.org/Style/CSS/current-work"&gt;technologies&lt;/a&gt; from which we &lt;a href="http://developer.mozilla.org/es4/"&gt;build interfaces&lt;/a&gt;, and any web developer worth her salt pays attention to valid, semantic markup and the very latest in CSS techniques and the newest developments in &lt;a href="http://alistapart.com/articles/behavioralseparation"&gt;unobtrusive scripting&lt;/a&gt; and &lt;a href="http://en.wikipedia.org/wiki/Representational_State_Transfer"&gt;REST&lt;/a&gt; and &lt;a href="http://microformats.org/"&gt;microformats&lt;/a&gt;, the pace of development in web-wide usability standards has been glacial at best.&lt;/p&gt;

&lt;p&gt;I bring this up because I've been noticing a slower adoption rate of highly-usable, widget-heavy, responsive, dynamic, configurable, powerful web applications.  My source?  Purely anecdotal and completely unscientific, among my friends and family and even coworkers at Zillow, who express frustration and antipathy toward websites for even minor perceived flaws, while clunky interfaces in &lt;a href="http://www.ebay.com/"&gt;other&lt;/a&gt;, &lt;a href="http://www.craigslist.org/"&gt;more&lt;/a&gt; &lt;a href="http://www.expedia.com/"&gt;primitive&lt;/a&gt; &lt;a href="http://www.myspace.com/"&gt;sites&lt;/a&gt; are tolerated and even preferred to their more elegant "web-application-y" counterparts.  With the exception of certain Google and Yahoo! applications, many powerful, innovative web apps are being ignored.  In the rush to push the browser to its limits, it's easy to lose sight of the end goal: making routine tasks easier for end users, &lt;em&gt;in the most straightforward way possible&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Web developers are a sensitive bunch - the profession long disregarded in the eyes of "serious" programmers.  Ajax was to change all of that.  And with impressive things now being done in one of the most challenging software development environments, the front-end of web development has finally been able to attract some formidable talent away from server-side, OS, and game development.  For the better, I should think, the Web has been gaining ground, not only as a place to exchange information, but as a valid, full-fledged platform for software development.  That's the idea behind all those standards out there:  eventually, if we clap our hands and work hard enough, the Web might supplant desktop-native applications for all but a few specialized purposes.  Soon, your computer will connect into the World Wide Continuum; your data will mingle freely with the data of billions around the world on an indistinguishable platform of desktop/web-hybrid applications, the social utopia of the Web will supplant thick-client, rugged-individualist desktop computing, the &lt;a href="http://singularity.com/"&gt;Singularity&lt;/a&gt; will occur, and we will all live in happy harmony with the universe.&lt;/p&gt;

&lt;p&gt;Fact of the matter is, not nearly enough consistency and code reuse is happening on the web.  To an extent, that's good.  I'd like to see the web remain a wild place that functions as laboratory as much as controlled platform.  But too often, problems are approached like they've never been addressed before.&lt;/p&gt;

&lt;p&gt;Sure, there are attempts at usability &lt;a href="http://www.userfocus.co.uk/articles/ISO23973.html"&gt;standards&lt;/a&gt; out there.  But web usability is complicated, and in spite of the best attempts of several &lt;a href="http://extjs.com/"&gt;javascript&lt;/a&gt; &lt;a href="http://www.jquery.com/"&gt;libraries&lt;/a&gt;, nobody, not even Google, is as slick and consistent as OS-native applications.&lt;/p&gt;

&lt;p&gt;When I'm designing an interface, I try to take into consideration three primary concerns:&lt;/p&gt;

&lt;dl&gt;
&lt;dt&gt;Familiarity.&lt;/dt&gt;&lt;dd&gt;If I haven't seen it before, I don't know what it does, and I don't want to use it, and may not even recognize it as part of the UI.  This becomes a huge obstacle for innovative interface development - more later.&lt;/dd&gt;
&lt;dt&gt;Consistency.&lt;/dt&gt;&lt;dd&gt;If a widget looks more than 70% similar to something else, I will expect them to behave the same.  This has ramifications &lt;em&gt;beyond your website&lt;/em&gt; (duh).&lt;/dd&gt;
&lt;dt&gt;Ease of use.&lt;/dt&gt;&lt;dd&gt;This is a big umbrella, encompassing everything from accesibility to ergonomics to "enjoyment": does my slider have a big enough click target?  Can I elect to use the keyboard to control this thingie, or am I stuck with the mouse?  Do I find myself repeating the same action for common tasks?  Is my path into common tasks streamlined and foolproof?&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;Back in the dark ages, before DHTML graduated from the shadows of image rollovers,  web interfaces were largely built out of browser-native form elements and links to more pages.  Usability was a minimal concern, because layouts were simpler and interaction models were much less ambitious.  Form interfaces were easy to manipulate, since they were largely designed to use OS-native widgets and behave comparable to their desktop-app counterparts.  Links were all blue-and-underlined, and they all took you to a new page.  Now that we've graduated from a website- to a webapplication-based web, however, many users haven't followed along.  Widgets that don't look like text-input boxes can be hard to spot; a recent usability study at Zillow found as much.  Part of the problem may be that many users simply haven't been exposed to web applications to expect that anything other than straightforward input controls to respond to input events.  I'd like to think that's part of our responsibility as web developers: challenge our users to explore, experiment, discover.  But it's also our responsibility to keep the guesswork out of our interfaces.&lt;/p&gt;

&lt;p&gt;Web usability is a moving target.  I don't have answers right now to many of these questions, but I'll be discussing them as they come up.  This post was to survey the territory; I hope to be able to explore aspects of this issue in greater detail soon.  I'll also be writing about the technical details of implementing a large-scale interface architecture that balances web standards with friendly, usable design.  I believe in powerful, flexible user interfaces, but only inasmuch as they empower the user.  Gratuitous lightboxes are not welcome!&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-7489586816823885344?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/7489586816823885344/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=7489586816823885344' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/7489586816823885344'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/7489586816823885344'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/07/web-native-ux.html' title='Web-Native UX'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-1577094914331699602</id><published>2007-06-30T17:48:00.000-07:00</published><updated>2007-06-30T17:53:55.169-07:00</updated><title type='text'>The Blogging Professional</title><content type='html'>&lt;blockquote&gt;It is a melancholy experience for a professional mathematician to find himself writing about mathematics. The function of a mathematician is to do something, to prove new theorems, to add to mathematics, and not to talk about what he or other mathematicians have done. Statesmen despise publicists, painters despise art-critics, and physiologists, physicists, or mathematicians have usually similar feelings: there is no scorn more profound, or on the whole more justifiable, than that of the men who make for the men who explain. Exposition, criticism, appreciation, is work for second-rate minds.&lt;/blockquote&gt;
&lt;p&gt;-- G. H. Hardy &lt;a href="http://www.math.ualberta.ca/~mss/books/A%20Mathematician's%20Apology.pdf"&gt;(PDF link)&lt;/a&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-1577094914331699602?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/1577094914331699602/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=1577094914331699602' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1577094914331699602'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1577094914331699602'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/06/it-is-melancholy-experience-for.html' title='The Blogging Professional'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-2475794724269738566</id><published>2007-06-23T19:31:00.000-07:00</published><updated>2009-03-04T13:13:42.145-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='ecmascript'/><category scheme='http://www.blogger.com/atom/ns#' term='crockford'/><category scheme='http://www.blogger.com/atom/ns#' term='python'/><title type='text'>Crockford's revisions to ECMAScript</title><content type='html'>&lt;p&gt;The ECMAScript standard is undergoing growing pains right now, and for the first time since 1999, it looks like major revisions to the language may take effect soon.  &lt;a href="http://www.crockford.com/javascript/recommend.html"&gt;Douglas Crockford has recommendations of his own.&lt;/a&gt;  While I don't have any strong feelings about the new features he requests (they can mostly be implemented in the JavaScript we already have), his Corrections are mostly sane things that will remove a lot of the suckiness from the current standard:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;Reserved words&lt;/li&gt;
&lt;li&gt;Standardize implementation of trailing commas in object literals and arrays&lt;/li&gt;
&lt;li&gt;Make &lt;kbd&gt;arguments&lt;/kbd&gt; object a true array&lt;/li&gt;
&lt;li&gt;Tail recursion optimization&lt;/li&gt;
&lt;li&gt;Deprecate primitive type wrappers, the &lt;kbd&gt;with&lt;/kbd&gt; statement, semicolon insertion, arguments.callee, typeof, and eval.&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;It looks like Brendan Eich is well aware of Crockford's longstanding laments, particularly against the &lt;kbd&gt;with&lt;/kbd&gt; statement and the reserved words restrictions, and &lt;a href="http://developer.mozilla.org/es4/spec/chapter_13_lexical_structure.html"&gt;has addressed them in the draft spec for ECMA-262 v4&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Crockford objects to restricting the use of reserved keywords in object definitions as unnecessary.  According to his argument, usage such as:&lt;/p&gt;

&lt;pre&gt;var o = {
    class: 'style', 
    with: function (n) { return n+'a top hat'; }
};
&lt;/pre&gt;

&lt;p&gt;will preserve the original use of these names, as the names are merely being used for scoped member properties; they cannot be confused in this context, syntactically, with the "official" uses of these keywords.  (This is, apparently, why the &lt;a href="http://json.org/"&gt;JSON spec&lt;/a&gt; requires double-quoted property names.)  However, it seems this restriction is in place mainly to make possible the use of the &lt;kbd&gt;with&lt;/kbd&gt; keyword; consider:&lt;/p&gt;

&lt;pre&gt;with (o) {
   class = 'none';
   with(class);
}
&lt;/pre&gt;

&lt;p&gt;If using these keywords were legal above, their use with the &lt;kbd&gt;with&lt;/kbd&gt; statement here invites syntax hell.&lt;/p&gt;

&lt;p&gt;I for one would be happy to see the demise of the &lt;kbd&gt;with&lt;/kbd&gt; keyword.  It breaks with JavaScript's function-level scoping, and creates more problems than it's meant to solve.  It "feels" uncharacteristic of the language, since it promotes an object's properties from "property" status into "variable" status, blending them indiscriminately into the surrounding scope.  JavaScript has enough trouble affording decent namespacing as it is; this "feature" smacks of bad practice.&lt;/p&gt;

&lt;p&gt;More than adding new features, we could use a "trimming-down" phase to the language.  While Mozilla toils at turning JavaScript into Python and other power-players at ECMA (presumably MS?) seek to remake the spec in the image of Java or C#, I'm mostly happy with the way it is - just please, for God's sake, fix the problems that are currently  there before bounding into a new round of ill-conceived feature bloat.&lt;/p&gt;

&lt;p&gt;We front-end developers are lucky to be enjoying a golden age of JavaScript.  But the clouds of confusion and incompatibility are on the horizon.  I don't see a compelling reason to add Java-style classical inheritance to the language, and I see even less a possibility of the vendors agreeing to ship timely implementations of this new spec.&lt;/p&gt;

&lt;p&gt;Surely wiser heads than mine are laboring on this problem.  But I guess I'm just not seeing that JavaScript falls short in ways that warrant changing it beyond recognition.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-2475794724269738566?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/2475794724269738566/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=2475794724269738566' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/2475794724269738566'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/2475794724269738566'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/06/crockfords-revisions-to-ecmascript.html' title='Crockford&apos;s revisions to ECMAScript'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-2177473232402653333.post-1522411357430456653</id><published>2007-06-05T16:36:00.000-07:00</published><updated>2009-03-04T13:14:13.537-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='javascript'/><category scheme='http://www.blogger.com/atom/ns#' term='code'/><category scheme='http://www.blogger.com/atom/ns#' term='history'/><category scheme='http://www.blogger.com/atom/ns#' term='html'/><title type='text'>Code Modularization and CSS Queries</title><content type='html'>&lt;p&gt;On his personal blog, YAHOO! front-end engineer Nicholas Zakas &lt;a href="http://www.nczonline.net/archive/2007/2/421"&gt;takes on&lt;/a&gt; the prevalence of the CSS query engine in &lt;a href="http://www.prototypejs.org/api/utility/dollar-dollar"&gt;client-side&lt;/a&gt; &lt;a href="http://docs.jquery.com/DOM/Traversing/Selectors"&gt;JavaScript&lt;/a&gt; &lt;a href="http://dojotoolkit.org/node/336"&gt;libraries&lt;/a&gt;.  The flexibility of some other quick syntax shorthand for accessing HTML elements and describing structure without using the conventional DOM methods has always appealed to me, so I wasn't sure what he was missing.&lt;/p&gt;

&lt;p&gt;Granted, the recent proliferation of JS libraries in general (and query engines in particular) has been rather excessive, no doubt tracking a &lt;a href="http://en.wikipedia.org/wiki/Boom_and_bust"&gt;boom and bust&lt;/a&gt; path.  I'm a fan of all of these efforts, though I wouldn't use most of them myself - they represent attempts to impose order onto the chaotic soup of client-side code that we've been dealing with since 1996.  Each one - Prototype's dubious extensions to native objects, jQuery's wrapping of DOM elements and telltale method chaining, Mochikit's Python-like syntax - represents a new experiment in turning the best approximation we've ever had to a cross-OS, open-source application platform - the web browser - into the full-fledged application framework it's destined to be.  Even if not all of these attempts have been useful, they've at least &lt;a href="http://www.quotationspage.com/quote/30392.html"&gt;been instructive&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After reading his replies to comments to his post, and after an informal conversation with &lt;a href="http://jgwebber.blogspot.com/"&gt;Joel Webber&lt;/a&gt; at the recent &lt;a href="http://code.google.com/events/developerday/mv-home.html"&gt;Google Developer Day&lt;/a&gt;, I realized that client-side techniques vary widely across organizations.&lt;/p&gt;

&lt;p&gt;At Zillow, for example, we go to great lengths to separate our HTML, CSS, and JavaScript.  Since our site is fairly complex and design-heavy, and code iterations happen with such frequency, this modularization is key to keeping sane.  So we have a set of rules we follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;span style="font-weight:bold;"&gt;All pieces of client-side code have a well-defined means for interacting with each other.&lt;/span&gt;  For CSS -&gt; HTML, it's CSS selectors.  For JS -&gt; HTML, it's the DOM.  For JS -&gt; CSS, it's classnames.  For HTML -&gt; JS, it's the DOM events model.&lt;/li&gt;

&lt;li&gt;&lt;span style="font-weight:bold;"&gt;Do not mix HTML, CSS, and JavaScript.&lt;/span&gt;  No inline styles, no inline event handlers, no creating elements via JavaScript.&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;You see where I'm going with this - HTML, CSS, and JavaScript are to exist independently of one another and "flip switches" rather than assemble by hand.  For the same reason you wouldn't write code like&lt;/p&gt;

&lt;pre&gt;function makeBoldRed(el) {
    el.style.fontWeight = 'bold';
    el.style.color = 'red';
}
&lt;/pre&gt;

&lt;p&gt;but rather&lt;/p&gt;

&lt;pre&gt;Element.addClass(el, CSS.ERROR_MESSAGE);
&lt;/pre&gt;

&lt;p&gt;I prefer to use CSS-style selectors to fix the gap, and allow JavaScript to refer to HTML using the same syntax that its presentation counterpart does.&lt;/p&gt;

&lt;p&gt;However, it appears that &lt;a href="http://www.yahoo.com"&gt;some&lt;/a&gt; &lt;a href="http://www.google.com"&gt;large&lt;/a&gt; &lt;a href="http://www.google.com"&gt;companies&lt;/a&gt; are generating HTML through JavaScript.  This can work too, but in this case your goals are going to be much different.  Once you start working this way, you risk accessibility and graceful degradation.  You also lose the ability to develop and test presentation-layer components independently of one another, and this can lead to a poorer design or, at the very least, longer development and testing time.  Another problem is debugging; if you start permitting your script to tinker with your HTML, or if your script depends on a fixed HTML structure to function properly, design changes can lead to interminable headaches when a complex script's dynamic change to the DOM structure goes undetected.&lt;/p&gt;

&lt;p&gt;Sure, CSS queries can be performance bottlenecks.  But usually they aren't, at least not when you're using a throughly-tested engine such as are available in the major JS libraries.  A minor performance hit - most query engines can perform the most complex queries in under 5 ms, depending on your machine - is a small price to pay for the flexibility of modular code.&lt;/p&gt;

&lt;p&gt;On a side note, I was most intrigued by Joel Webber's description of the &lt;a href="http://code.google.com/webtoolkit/"&gt;Google Web Toolkit's&lt;/a&gt; function: to add a compile-time step to optimize and inline JavaScript code.  I can appreciate that.  Certainly, a well-written compiler will be able to optimize code to a greater degree than a meticulous programmer.  But why Java?  Why apply this essentially alien style of programming to the client?  This question remains outstanding for me.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/2177473232402653333-1522411357430456653?l=davidgolightly.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://davidgolightly.blogspot.com/feeds/1522411357430456653/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=2177473232402653333&amp;postID=1522411357430456653' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1522411357430456653'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/2177473232402653333/posts/default/1522411357430456653'/><link rel='alternate' type='text/html' href='http://davidgolightly.blogspot.com/2007/06/code-modularization-and-css-queries.html' title='Code Modularization and CSS Queries'/><author><name>David Golightly</name><uri>http://www.blogger.com/profile/15826032679447528405</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry></feed>
