Site Search

Michael Stone, February 20, 2012, , (src)

Several folks have recently asked me how my “Search site” box works, so here’s a quick writeup on the implementation as it stands at the time of this writing:

  1. The current site is entirely static and is built by calling pandoc from a top-level non-recursive site Makefile, written in GNU Make.

  2. The main inputs to this Makefile are “pages”, which are represented as directories each containing a file named index.txt containing the pandoc Markdown source for the page.

  3. When run, my Makefile uses find to locate page directories and then uses pandoc to convert the index.txt page sources into index.html outputs. Additionally, a tiny script named jsonify.py, reproduced below:

    #!/usr/bin/env python
    from __future__ import with_statement
    import sys
    import cjson
    
    data = {}
    
    for fn in sys.argv[1:]:
        with open(fn) as f:
            data[fn.replace('index.txt', '')] = f.read()
    
    print cjson.encode(data)

    is used to build a target called site.json containing a JSON-encoded representation of all the source texts for all the pages in the site indexed by the name of the “page” directory that contains them.

  4. Next, my Makefile uses pandoc with a custom HTML template to produce HTML from my page sources that contains a text input field named #searchbox (initially hidden by CSS) and that runs the following JavaScript program during page rendering:

    (function($){
    $(document).ready(function(){
      var site;
      $.getJSON('/site.json', function(data){
        site = data;
        $("#searchbar").css("display", "inline-block");
      });
      var doSearch;
      doSearch = function(){
        var search;
        search = $("#searchbox").val();
        if (search.length > 0) {
          var results, pat;
          pat = RegExp(search);
          results = [];
          $.each(site, function(k, v){
            if (v.match(pat)) {
              results.push('<li><a href="/' + k + '">' + v.split(/\n/)[0].replace(/^% /, '') + '</a></li>');
            }
          });
          if (results.length > 0) {
            $("#searchresults").html('<br/><h2>Matching Posts</h2><ul>' + results.join('') + '</ul>');
          } else {
            $("#searchresults").html('<br/><h2>Matching Posts</h2><p>None</p>');
          }
        }
        else {
          $("#searchresults").empty();
        }
      };
      $("#searchbox").keyup(doSearch);
    });})(jQuery);
  5. When run, this script uses the jQuery library to asynchronously fetch site.json, to unhide the searchbox on success, and to wait for jQuery keyup events.

    keyup events are then handled by selecting keys from the previously fetched site JavaScript object whose values are matched by the pat JavaScript regexp (which was, in turn, built from the value of the #searchbox text field).

    Finally, the matching object keys are transformed into links and added to the #searchresults element’s innerHTML.