Loading GitHub Gists After The Page Content Has Loaded
A long time ago, I created a syntax highlighter / color-coder for my blog post code snippets. It was a pain to create; but in the end, it worked pretty well. For ColdFusion code. That was tag based. In recent years, however, I've tried to branch out a bit, playing with other languages, JavaScript in particular. For these languages, my color coding sucks. As such, I've been on the hunt for other color coding options. There's a ton of methods out there; but, I need one that can seamlessly integrate with my particular blog authoring workflow. From what I've read so far, it looks like GitHub's Gists might be the best option for my code. Do to the way I store my blog content, however, I don't want to necessarily load Gists inline with the rest of my text. Instead, I'd like to load the GitHub Gists after the DOM has loaded and then inject them (replace them) into the currently rendered post.
When you write a Gist on GitHub, the gist provides you with a Script tag that can be used to embed the gist content in a 3rd party site. So, for example, my first Gist, which I created for testing purposes, can be loaded with the following script tag:
<script src="https://gist.github.com/1600811.js"></script>
This script tag loads a remote JavaScript file which embeds the gist using two document.write() method calls:
Embed Code For Remote GitHub Gist
document.write( " .... LINK TAG FOR GIST CSS .... " );
document.write( " .... GIST HTML CONTENT .... " );
This is fine when the script tag is placed inline with the rest of your content; but, if this script is loaded after DOM-Ready, the calls to document.write() will actually erase the contents of currently rendered HTML page.
To get around this, I created a jQuery plugin that changes the definition of document.write() for the duration of the script load. This way, when the remote Gist / script content is loaded, document.write() writes to a custom method rather than the native write() method. While I believe the loading of script content is synchronous, I've encapsulated the response in a jQuery Deferred object so that the calling code can easily bind to resolved and rejected outcomes of the HTTP request.
Before we take a look at the jQuery plugin that wraps the gist, let's look at the demo that is using it. In the following code, I'm loading a given Gist after the DOM has loaded. I'm then explicitly injecting the Gist back into the document.
<!DOCTYPE html>
<html>
<head>
<title>Loading GitHub Gists After Page Content Has Loaded</title>
<style type="text/css">
.gist,
pre {
font-size: 12px ;
}
</style>
<!-- Load the jQuery library a and the Gist plugin. -->
<script type="text/javascript" src="../jquery-1.7.1.js"></script>
<script type="text/javascript" src="./jquery.gist.js"></script>
<!-- Load the Gist after DOM ready. -->
<script type="text/javascript">
$(function(){
// Get the placeholder.
var placeholder = $( "#placeholder" );
// Get the gist with the given ID. This will come back
// as both a hash of file names and an ordered array.
var gistResult = $.getGist( "1600811" );
// When the gist has loaded, append the contents to the
// rendered DOM.
gistResult.done(
function( gist ){
// Empty the placeholder.
placeholder.empty();
// Get the ordered files.
var ordered = gist.ordered;
// Add each gist to the content.
for (var i = 0 ; i < ordered.length; i++){
// Add a title for the gist.
placeholder.append(
"<h3>" + ordered[ i ].fileName + "</h3>"
);
// Add the gist content.
placeholder.append( ordered[ i ].content );
}
}
);
});
</script>
</head>
<body>
<h1>
Loading GitHub Gists After Page Content Has Loaded
</h1>
<h2>
Gists From GitHub:
</h2>
<div id="placeholder">
Loading...
</div>
<p>
This page has finised loading.
</p>
</body>
</html>
As you can see, we have defined a jQuery plugin, getGist(), which takes the gist ID and returns a promise. This promise will ultimately resolve to a hash of file names and their color-coded content.
Now, let's take a look at how getGist() works.
jquery.gist.js - Our jQuery Plugin For Gist Loading
// Define a sandbox in which to create the Gist loader jQuery plugin.
// This Gist loader only works with public gists. It will load all of
// the files (in a single request) and then return an array of loaded
// files (with the ability to access by file name).
(function( $, nativeWrite ){
// When the Gist comes back, the first call to the write() method
// writes out the stylesheet.
var writeStyleSheetLink = function( value ){
// If the stylesheet has not been written before, then append
// it to the head. Since all the Gists use the same
// stylesheet, we only have to do this once per page.
if (!stylesheetIsEmbedded){
// Append the stylesheet Link tag.
$( "head:first" ).append( value );
// Flag the embed so we don't write the Link tag twice.
stylesheetIsEmbedded = true;
}
// Change the write() method for gist content production.
document.write = writeGistContent;
};
// The second write to the document will be for the complete gist
// content. At this point, we have to parse it out and organize
// it in a structure.
var writeGistContent = function( value ){
// Reset the files (container) we are about to compile.
files = {};
// We'll also want to list the files by Index, if the user
// wants that information.
files.ordered = [];
// Parse the Gist HTML in a local DOM tree.
var gistContent = $( value );
// Get all of the files in the gist.
gistContent.find( "div.gist-file" ).each(
function(){
// Get a jQuery reference to the current gist node.
var gistFile = $( this );
// Get the name of the file. For this, we will return
// the content of the first Meta anchor that doesn't
// contain a syntactic link.
var metaTags = gistFile.find( "div.gist-meta a" )
.filter(
function(){
// Only keep this value if it doesn't
// contain a useless value.
return( $( this ).text().search( new RegExp( "^\\s*(view raw|this gist|github)", "i" ) ) === -1 );
}
)
;
// Get the file name from the first filtered Meta
// anchor tag.
var fileName = $.trim( metaTags.first().text() );
// Get the content of the file. Each file will need
// to be re-wrapped in its own Gist div.
var content = $( "<div class='gist'></div>" )
.append( gistFile )
;
// Add the file the collection, indexed by name.
files[ fileName ] = {
fileName: fileName,
content: content
};
// Add this file to the "ordered" list as well.
files.ordered.push( files[ fileName ] );
}
);
// NOTE: At this point, the [files] value has been populated
// and will be used in the success() callback of the AJAX
// request.
};
// I flag whether or not a stylesheet has been appending to the
// current document. Since all Gist requests share the same
// style, we can disregard all subsequent Link tags.
var stylesheetIsEmbedded = false;
// I am the active result for the gist content request.
var files = null;
// Define the actual script loader.
$.getGist = function( gistID ){
// Before the Script is requested, override the native
// write() method so that we can intercept the calls
// for the Gist stylesheet and content output.
document.write = writeStyleSheetLink;
// Create a deferred value for our Gist content.
var result = $.Deferred();
// Request the remote Script (that will write out the
// Gist content).
$.ajax({
url: ("https://gist.github.com/" + gistID +".js"),
dataType: "script",
success: function(){
// Resolve the promise with the compiled Gist files.
result.resolve( files );
},
error: function( jqXHR, status, error ){
// Reject the promise.
result.reject( status, error );
},
complete: function(){
// Change the write() method back to the native
// write(). If we do it in the complete callback,
// then we won't have to worry about HTTP issues.
document.write = nativeWrite;
}
});
// Return the promise of the gist.
return( result.promise() );
};
})( jQuery, document.write );
Because GitHub's embed code uses document.write(), we have to change the way document.write() works at the time it is invoked. To do this, we override the native write() method with our own internal write() methods. The first method reassignment we use knows that the Gist attempts to write a Link tag for the color-coded stylesheet. The second method reassignment we use knows that the Gist attempts to write the Gist HTML to the current document.
Once the two write() calls have been invoked by the Gist embed code, the document.write() method is reassigned to the native method.
This works pretty well. But, the big danger here is that we are relying on an algorithm that is outside of our control. Should Git change the way its Gist embed script works, our Gist plugin will very likely break.
So far, GitHub's Gist functionality seems like the one I can most easily integrate into my blog authoring workflow. It has a back-end API that will allow me automatically create Gists; and, it has a fairly straightforward way for me to embed those Gists in my content. The color coding for the code is not perfect - it attempts to guess the language based on the file name extension; and, as you can see in the video, it's a little funky. That said, "funky" is still much better than what I have in place.
Want to use code from this post? Check out the license.
Reader Comments
Checkout the github/mylyn connector also.
Gists are also supported, you can now highlight text in the editor or select a file from the explorer and run Create Gist from the context menu. A Gist will be created in the background and a notification popup will display after it has been created with a link to the newly created Gist.
https://github.com/blog/852-github-mylyn-connector-for-eclipse
Wouldn't it be more ideal to store the id of the gist on the div which will contain it? This your HTML could be more appropriately written:
<div class="placeholder" data-gistID="1600811">Loading...</div>
Then you don't need to include ANY JS code on your page other than initializing the plugin.
@Mike,
Dang, that's pretty cool! My first line of thinking was to have the code in the blog post (raw version). Then, when I save it in my blog CMS, the code would be parsed out and posted to GitHub Gists. This way, as a save / update the blog post, the Gists can be automatically updated as well. Still just thoughts floating around in my head. Doing it right from Eclipse would be really cool, too!
@Andy,
You make a really good point. Ultimately, when/if I try to integrate this into my blog CMS, I think I would store the Gist ID with the blog record. Then, each "snippet" within the blog post could be a different "file" within the Gist.
To do that, I would associate the filename with the placeholder:
<div data-gist-filename="index.cfm"> ... </div>
Although, it's probably a good idea to store the ID and the filename with the div, just for some sense of elegance.
I'm still digging around and trying to think about how I want these to all come together.
Does this mean that you'll give up all those white spaces? ;-)
@Lola,
Pfffff, white-space ain't going anywhere :D
Ben, you're really making some fantastic headway with the deferred loading and constructing of objects. Kudos, sir.
I just might try to whip this up into a WordPress plugin (with your permission, of course) if I get some free time over the 3-day weekend. This would be highly preferable to the in-line code and SyntaxHighlighter combination I'm using right now. :D
Keep the awesome ideas coming!
Ben,
You forgot an F.
I think you mean "pFFFFFF white-space ain't going anywhere." ;-) But maybe since this is a CF blog, you really meant to say "##FFFFFF"
The syntax highlighting seems to be very minimal in a Gist, it doesn't highlight enough in my opinion. For example, coloring strings, special keywords such as var, etc. I still have plans to roll my own one day built into my markdown editor.
Check out my repo where I load up all my GitHub data (repos, gists) asynchronously using RequireJs, when.js, a Js wrapper around the GitHub REST API (v2). Works like a charm: https://github.com/Integralist/integralist.github.com
@Todd,
Thanks my man - I'm really loving the Deferred logic. Please, feel free to use this code in whatever manner you like. I'm super pumped up to hear that it might add value elsewhere :)
@Randall,
Ha ha ha, I love it!
@Kevin,
I tend to agree with you. Mostly, I am going with it because it 1) color-codes comments appropriately, which goes a long way for readability and, 2) can be fairly easily integrated with existing workflow. That said, there is definitely something to be desired about the color coding.
@Mark,
That looks super robust! Thanks for the link.
@All,
I tried to re-work this approach using IFrame in order to "sandbox" the mutation of document.write():
www.bennadel.com/blog/2316-Using-An-IFrame-To-Override-document-write-Inside-A-DOM-Document-Object-Model-Sandbox.htm
Now, the Gist script loading and document.write() mutation happen in the context of an IFrame, leaving the parent document unchanged.