Check if plugin is defined before running update on $.ajaxComplete

/*
 * jQuery Thead Plugin v${version}
 * http://www.asual.com/jquery/thead/
 *
 * Copyright (c) 2009-2010 Rostislav Hristov
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * Date: ${timestamp}
 */
(function($) {

    var CLAZZ = 'class',
        CELLPADDING = 'cellpadding',
        CELLSPACING = 'cellspacing',
        _agent = navigator.userAgent,
        _w = $(window),
        _d = $(document),
        _tables = [],
        _magicNumber = 4,
        _supported = !($.browser.msie && parseFloat(_agent.substr(_agent.indexOf('MSIE') + _magicNumber)) < 7),
        _interval = null,
        _parseInt = function(value) {
            var result = parseInt(value, 10);
            return isNaN(result) ? 0 : result;
        },
        _scroll = function() {
            $(_tables).each(function() {
                var w, s = 'thead tr th, thead tr td',
                    t = $('table', this.parent().prev()).get(0),
                    c = $('caption', t),
                    collapse = $(t).css('border-collapse') == 'collapse',
                    ths = $(s, t),
                    offset = _d.scrollTop() - $(t).offset().top + _magicNumber;
                if (c.length) {
                    offset -= c.get(0).clientHeight;
                }
                $(s, this).each(function(index) {
                    var th = ths.eq(index).get(0);
                    w = $(th).css('width');
                    $(this).css('width', w != 'auto' ? w : th.clientWidth - _parseInt($(th).css('padding-left')) - _parseInt($(th).css('padding-right')) + 'px');
                });
                $(this).css({
                    display: (offset > _magicNumber && offset < t.clientHeight - $('tr:last', t).height() - _magicNumber*2) ? $(t).css('display') : 'none',
                    left: $(t).offset().left - _d.scrollLeft() + 'px',
                    width: $(t).get(0).offsetWidth
                });
                if (!$.support.boxModel) {
                    $(this).css({
                        top: _d.scrollTop()
                    });
                }
            });
        };

    $(function() {
        if (_supported) {
            _w.scroll(_scroll).resize(function() {
                if (!_interval) {
                    _interval = setInterval(function() {
                        if (_interval) {
                            _interval = clearInterval(_interval);
                        }
                        _scroll();
                    }, 50);
                }
            });
            $(document).ajaxComplete(function() {
                $.thead.update();
            });
            $('.jquery-thead').thead();
        }
    });

    $.thead = (function () {
        return {
            update: function() {
                $(_tables).each(function() {
                    var base = $('thead', $('table.jquery-thead, table', this.parent().prev()).get(0));
                    var local = $('thead', this);
                    if (local.html() != base.html()) {
                        local.parent().append(base.clone(true));
                        local.remove();
                        _scroll();
                    }
                });
            }
        };
    })();

    $.fn.thead = function() {
        if (_supported) {
            $(this).each(function() {
                var table = this.tagName.toLowerCase() == 'table' ? $(this) : $('table', this),
                    parent = table.parent(),
                    thead = $('thead', table);
                if (thead.length) {
                    var clazz = table.attr(CLAZZ),
                        cp = table.attr(CELLPADDING),
                        cs = table.attr(CELLSPACING);
                    _tables.push($('<table />').attr(CLAZZ, clazz)
                            .attr(CELLPADDING, cp ? cp : 1)
                            .attr(CELLSPACING, cs ? cs : 2)
                            .css({position: $.support.boxModel ? 'fixed' : 'absolute', top: 0}).appendTo($('<' + parent.get(0).tagName + '/>')
                                    .attr(CLAZZ, parent.attr(CLAZZ)).insertAfter(parent)).append($(thead).clone(true)));
                }
            });
        }
        _scroll();
    };

})(jQuery);

/*
 * jQuery Thead Plugin v${version}
 * http://www.asual.com/jquery/thead/
 *
 * Copyright (c) 2009-2010 Rostislav Hristov
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * Date: ${timestamp}
 */
(function($) {

    var CLAZZ = 'class',
        CELLPADDING = 'cellpadding',
        CELLSPACING = 'cellspacing',
        _agent = navigator.userAgent,
        _w = $(window),
        _d = $(document),
        _tables = [],
        _magicNumber = 4,
        _supported = !($.browser.msie && parseFloat(_agent.substr(_agent.indexOf('MSIE') + _magicNumber)) < 7),
        _interval = null,
        _parseInt = function(value) {
            var result = parseInt(value, 10);
            return isNaN(result) ? 0 : result;
        },
        _scroll = function() {
            $(_tables).each(function() {
                var w, s = 'thead tr th, thead tr td',
                    t = $('table', this.parent().prev()).get(0),
                    c = $('caption', t),
                    collapse = $(t).css('border-collapse') == 'collapse',
                    ths = $(s, t),
                    offset = _d.scrollTop() - $(t).offset().top + _magicNumber;
                if (c.length) {
                    offset -= c.get(0).clientHeight;
                }
                $(s, this).each(function(index) {
                    var th = ths.eq(index).get(0);
                    w = $(th).css('width');
                    $(this).css('width', w != 'auto' ? w : th.clientWidth - _parseInt($(th).css('padding-left')) - _parseInt($(th).css('padding-right')) + 'px');
                });
                $(this).css({
                    display: (offset > _magicNumber && offset < t.clientHeight - $('tr:last', t).height() - _magicNumber*2) ? $(t).css('display') : 'none',
                    left: $(t).offset().left - _d.scrollLeft() + 'px',
                    width: $(t).get(0).offsetWidth
                });
                if (!$.support.boxModel) {
                    $(this).css({
                        top: _d.scrollTop()
                    });
                }
            });
        };

    $(function() {
        if (_supported) {
            _w.scroll(_scroll).resize(function() {
                if (!_interval) {
                    _interval = setInterval(function() {
                        if (_interval) {
                            _interval = clearInterval(_interval);
                        }
                        _scroll();
                    }, 50);
                }
            });
            $(document).ajaxComplete(function() {
                if ('undefined' != typeof $.thead) {
                    $.thead.update();
                }
            });
            $('.jquery-thead').thead();
        }
    });

    $.thead = (function () {
        return {
            update: function() {
                $(_tables).each(function() {
                    var base = $('thead', $('table.jquery-thead, table', this.parent().prev()).get(0));
                    var local = $('thead', this);
                    if (local.html() != base.html()) {
                        local.parent().append(base.clone(true));
                        local.remove();
                        _scroll();
                    }
                });
            }
        };
    })();

    $.fn.thead = function() {
        if (_supported) {
            $(this).each(function() {
                var table = this.tagName.toLowerCase() == 'table' ? $(this) : $('table', this),
                    parent = table.parent(),
                    thead = $('thead', table);
                if (thead.length) {
                    var clazz = table.attr(CLAZZ),
                        cp = table.attr(CELLPADDING),
                        cs = table.attr(CELLSPACING);
                    _tables.push($('<table />').attr(CLAZZ, clazz)
                            .attr(CELLPADDING, cp ? cp : 1)
                            .attr(CELLSPACING, cs ? cs : 2)
                            .css({position: $.support.boxModel ? 'fixed' : 'absolute', top: 0}).appendTo($('<' + parent.get(0).tagName + '/>')
                                    .attr(CLAZZ, parent.attr(CLAZZ)).insertAfter(parent)).append($(thead).clone(true)));
                }
            });
        }
        _scroll();
    };

})(jQuery);


new style base

require 'rubygems'
require 'sinatra/base'
require 'slim'
require 'sass'
require 'mongoid'

Mongoid.load!("config/mongoid.yml")

Slim::Engine.set_default_options :sections => true

class App < Sinatra::Base

  set :public, File.join(File.dirname(__FILE__), 'public')
  set :views, File.join(File.dirname(__FILE__), 'views')

  helpers do
    def partial(page, options={})
      haml page, options.merge!(:layout => false)
    end
  end

  get('/'){ slim :layout}

end

require 'rubygems'
require 'sinatra/base'
require 'slim'
require 'sass'
require 'mongoid'

Mongoid.load!("config/mongoid.yml")

Slim::Engine.set_default_options :sections => false

class App < Sinatra::Base

  set :public, File.join(File.dirname(__FILE__), 'public')
  set :views, File.join(File.dirname(__FILE__), 'views')

  helpers do
    def partial(page, options={})
      haml page, options.merge!(:layout => false)
    end
  end

  get('/') do
    slim :index
  end

  get('/styles') do
    slim :styles
  end

end

$(document).ready(function() {
  /*FORM*/
  $('input, textarea, select').focusin(function(){
    if($(this).val() == ''){
      $(this).parent('.input-placeholder').children('label.label_input').hide();
    }
  });

  $('input, textarea, select').focusout(function(){
    if($(this).val() == ''){
      $(this).parent('.input-placeholder').children('label.label_input').show();
    }
  });

});

/*
Syntax error: Invalid CSS after "...nge: background": expected selector or at-rule, was ": $orange; /* f..."
        on line 11 of public/stylesheets/styles.scss

6: $vine: #ac3900;
7: $white: #ffffff;
8: $black: #000000;
9: $lightgrey: #e6e5e5;
10: $textgrey: #959494;
11: $gradientorange: background: $orange; /* for non-css3 browsers */
12: filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='$orange', endColorstr='$orangered'); /* for IE */
13: background: -webkit-gradient(linear, left top, left bottom, from($orange), to($orangered)); /* for webkit browsers */
14: background: -moz-linear-gradient(top,  $orange,  $orangered); /* for firefox 3.6+ */
15: ;
16:

Backtrace:
public/stylesheets/styles.scss:11
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/scss/parser.rb:1130:in `expected'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/scss/parser.rb:1066:in `expected'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/scss/parser.rb:28:in `parse'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/engine.rb:342:in `_to_tree'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/engine.rb:315:in `_render'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/engine.rb:262:in `render'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/plugin/compiler.rb:340:in `update_stylesheet'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/plugin/compiler.rb:202:in `block in update_stylesheets'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/plugin/compiler.rb:200:in `each'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/plugin/compiler.rb:200:in `update_stylesheets'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/lib/sass/plugin/compiler.rb:298:in `block in watch'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/vendor/listen/lib/listen/multi_listener.rb:86:in `call'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/vendor/listen/lib/listen/multi_listener.rb:86:in `on_change'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/vendor/listen/lib/listen/multi_listener.rb:95:in `block in initialize_adapter'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/vendor/listen/lib/listen/adapters/polling.rb:55:in `call'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/vendor/listen/lib/listen/adapters/polling.rb:55:in `poll'
/Users/bellatrix/.rvm/gems/ruby-1.9.3-p0@bartender/gems/sass-3.2.1/vendor/listen/lib/listen/adapters/polling.rb:31:in `block in start'
*/
body:before {
  white-space: pre;
  font-family: monospace;
  content: "Syntax error: Invalid CSS after \"...nge: background\": expected selector or at-rule, was \": $orange; /* f...\"\A         on line 11 of public/stylesheets/styles.scss\A \A 6: $vine: #ac3900;\A 7: $white: #ffffff;\A 8: $black: #000000;\A 9: $lightgrey: #e6e5e5;\A 10: $textgrey: #959494;\A 11: $gradientorange: background: $orange; /* for non-css3 browsers */\A 12: filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='$orange', endColorstr='$orangered'); /* for IE */\A 13: background: -webkit-gradient(linear, left top, left bottom, from($orange), to($orangered)); /* for webkit browsers */\A 14: background: -moz-linear-gradient(top,  $orange,  $orangered); /* for firefox 3.6+ */ \A 15: ;\A 16: "; }

body
:background black
h1, h2, h3, h4 {
  font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
  font-weight: lighter;
  font-size: 80px; }


Added product restoration functionality

Rails.application.routes.draw do
  # Add your extension routes here
end

module ProductExpiration
  def self.included(base)
    base.named_scope :not_deleted, lambda { { :conditions => ["(deleted_at IS NULL OR deleted_at > ?)", Time.now.utc.to_s(:db)] } }
    base.class_eval do
      def deleted?
        deleted_at && deleted_at <= Time.now
      end
    end
  end
end

require 'spree_core'
require 'spree_product_expiration_hooks'

module SpreeProductExpiration
  class Engine < Rails::Engine

    config.autoload_paths += %W(#{config.root}/lib)

    def self.activate
      Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
        Rails.env.production? ? require(c) : load(c)
      end
      Product.send(:include, ProductExpiration)
    end

    config.to_prepare &method(:activate).to_proc
  end
end

class SpreeProductExpirationHooks < Spree::ThemeSupport::HookListener
  # custom hooks go here
end

Rails.application.routes.draw do
  # Add your extension routes here
  namespace :admin do
    resources :products do
      get 'restore', :on => :member
    end
  end
end

module ProductExpiration
  def self.included(base)
    base.named_scope :not_deleted, lambda { { :conditions => ["(products.deleted_at IS NULL OR products.deleted_at > ?)", Time.now.utc.to_s(:db)] } }
    base.class_eval do
      def deleted?
        deleted_at && deleted_at <= Time.now
      end
    end
  end
end
module ProductExpirationController
  def restore
    @product = Product.find_by_permalink(params[:id])
    @product.deleted_at = nil

    if @product.save
      self.notice = I18n.t("notice_messages.product_restored")
    else
      self.notice = I18n.t("notice_messages.product_not_restored")
    end

    redirect_to edit_admin_product_url(@product)
  end
end

require 'spree_core'
require 'spree_product_expiration_hooks'

module SpreeProductExpiration
  class Engine < Rails::Engine

    config.autoload_paths += %W(#{config.root}/lib)

    def self.activate
      Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
        Rails.env.production? ? require(c) : load(c)
      end
      Product.send(:include, ProductExpiration)
      Admin::ProductsController.send(:include, ProductExpirationController)

      Admin::BaseHelper.class_eval do
        def link_to_restore(resource, options = {})
          link_to_with_icon('restore', t("restore"), restore_admin_product_url(resource))
        end
      end
    end

    config.to_prepare &method(:activate).to_proc
  end
end

class SpreeProductExpirationHooks < Spree::ThemeSupport::HookListener
  # custom hooks go here
  insert_after :admin_product_form_right, 'admin/products/expiration'
  insert_before :admin_products_index_row_actions, 'admin/products/restore_link'
end


undo ;)

var COLS = 3;
var ROWS = 3;
var LEGAL_MOVES = [];
var save;
var freshBoard;

Event.observe(window, 'load', function() {
  $('regenerate').observe('click', setupGame);
  $('restart').observe('click', function() {restoreState(freshBoard)});
  $('save').observe('click', function() {save = saveState()});
  $('load').observe('click', function() {restoreState(save)});
  setupGame();
});

function setupGame() {
  generateBoard();
  generateGame();
  setHandlerForLegalMoves();
  save = freshBoard = saveState();
}

function move(event) {
  var element = event.element();

  element.addClassName('visited');
  element.appendChild($('knight'));

  LEGAL_MOVES.each(function(item) { $(item).stopObserving('click'); });

  setHandlerForLegalMoves(element);

  if(isBoardFinished()) {
    ROWS++;
    COLS++;
    setupGame();
  }
}

function isBoardFinished() {
  return(
    $$('.valid').reject(function(cell) {
      return cell.hasClassName('visited');
    }).length < 1
  );
}

function setHandlerForLegalMoves(element) {
  var element = $('knight').parentNode
  legalMoves(element).each(function(item) {
    $(item).observe('click', move);
  });
}

function legalMovesForGeneration(cell) {
  return [ cell.id.offset(1, 2), cell.id.offset(2, 1),
           cell.id.offset(-1, 2), cell.id.offset(-2, 1),
           cell.id.offset(-1, -2), cell.id.offset(-2, -1),
           cell.id.offset(1, -2), cell.id.offset(2, -1)
         ].compact().reject(function(item) {
           return ($(item).hasClassName('visited'));
         });
}

function legalMoves(cell) {
  LEGAL_MOVES = legalMovesForGeneration(cell).select(function(item) {
    return ($(item).hasClassName('valid') && !$(item).hasClassName('visited'));
  });
  return LEGAL_MOVES;
}

String.prototype.col = function() {
  return parseInt(this.slice(this.indexOf('_') + 1, this.lastIndexOf('_')));
}

String.prototype.row = function() {
  return parseInt(this.slice(this.lastIndexOf('_') + 1));
}

String.prototype.offset = function(x, y) {
  var idCol = this.col() + x;
  var idRow = this.row() + y;

  if(idCol > COLS || idRow > ROWS || idCol < 1 || idRow < 1) {
    return null;
  } else {
    return '_' + idCol + '_' + idRow;
  }
}

function generateBoard() {
  $$('.row').invoke('remove');
  ROWS.times(function(j) {
    var rowNum = j + 1;
    var row = $(document.createElement('div'));
    row.addClassName('row');
    row.writeAttribute('id', 'row' + rowNum);

    COLS.times(function(i) {
      var colNum = i + 1;
      var cell = $(document.createElement('div'));
      cell.addClassName('cell');
      cell.writeAttribute('id', '_' + colNum + '_' + rowNum);

      if(colNum == 1) cell.addClassName('first');
      if(colNum == COLS) cell.addClassName('last');
      if(rowNum == 1) cell.addClassName('bottom');
      if(rowNum == ROWS) cell.addClassName('top');

      row.appendChild(cell);
    });
    $('board').insert( { top: row } );
  });
}

function generateGame() {
  do {
    var startCell = $('_' + rand(COLS) + '_' + rand(ROWS));
  } while(legalMovesForGeneration(startCell).length == 0);

  var knight = $(document.createElement('img'));

  startCell.addClassName('visited');

  knight.writeAttribute('src', 'black_knight.png');
  knight.writeAttribute('id', 'knight');
  startCell.insert( { top: knight } );

  findNextCell(startCell, 1);
  setHandlerForLegalMoves($('knight').parentNode);
}

function findNextCell(cell, alreadyFound) {
  if( alreadyFound > (ROWS * COLS) * (5.0 / 8.0) || legalMovesForGeneration(cell).length == 0) {
    return;
  } else {
    var moves = legalMovesForGeneration(cell);
    var nextCell = $(moves[ rand(moves.length) - 1 ]);

    if(nextCell.hasClassName('valid')) {
      findNextCell(nextCell, alreadyFound);
    }
    else {
      nextCell.addClassName('valid');
      findNextCell(nextCell, alreadyFound + 1);
    }
  }
}

function saveState() {
  return {
    visited: $$('.visited'),
    current: $('knight').parentNode
  }
}

function restoreState(state) {
  if (!state) return;

  $$('.valid').each(function(item) { $(item).stopObserving('click') });
  $$('.visited').each(function(cell) { cell.removeClassName('visited') });
  state['visited'].each(function(cell) { cell.addClassName('visited') });

  var knight = $('knight');
  state['current'].appendChild(knight);
  setHandlerForLegalMoves(knight.parentNode)
}

function rand(max) {
  return Math.floor(Math.random() * max + 1);
}

<!DOCTYPE html PUBLIC "-//W3C/DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/DTD/strict.dtd">

    <link rel="stylesheet" href="style.css" type="text/css" />

      <input type="button" value="Re-Generate" id="regenerate" />
      <input type="button" value="Restart" id="restart" />
      <input type="button" value="Load" id="load" />
      <input type="button" value="Save" id="save" />
      <input type="button" value="Undo" id="undo" />

var COLS = 3;
var ROWS = 3;
var LEGAL_MOVES = [];
var save;
var freshBoard;
var stateStack = [];

Event.observe(window, 'load', function() {
  $('regenerate').observe('click', setupGame);
  $('restart').observe('click', function() {restoreState(freshBoard)});
  $('save').observe('click', function() {save = saveState()});
  $('load').observe('click', function() {restoreState(save)});
  $('undo').observe('click', function() {restoreState(stateStack.pop())});
  setupGame();
});

function setupGame() {
  generateBoard();
  generateGame();
  setHandlerForLegalMoves();
  stateStack = [];
  save = freshBoard = saveState();
}

function move(element, allowWinning) {
  if (allowWinning == undefined) allowWinning = true;
  stateStack.push(saveState());

  element.addClassName('visited');
  element.appendChild($('knight'));

  LEGAL_MOVES.each(function(item) { $(item).stopObserving('click'); });

  setHandlerForLegalMoves(element);

  if(isBoardFinished() && allowWinning) {
    advanceBoard();
  }
}

function advanceBoard() {
  ROWS++;
  COLS++;
  setupGame();
}

function isBoardFinished() {
  return(
    $$('.valid').reject(function(cell) {
      return cell.hasClassName('visited');
    }).length < 1
  );
}

function setHandlerForLegalMoves(element) {
  var element = $('knight').parentNode
  legalMoves(element).each(function(item) {
    $(item).observe('click', function(event) { move(event.element()) });
  });
}

function legalMovesForGeneration(cell) {
  return [ cell.id.offset(1, 2), cell.id.offset(2, 1),
           cell.id.offset(-1, 2), cell.id.offset(-2, 1),
           cell.id.offset(-1, -2), cell.id.offset(-2, -1),
           cell.id.offset(1, -2), cell.id.offset(2, -1)
         ].compact().reject(function(item) {
           return ($(item).hasClassName('visited'));
         });
}

function legalMoves(cell) {
  LEGAL_MOVES = legalMovesForGeneration(cell).select(function(item) {
    return ($(item).hasClassName('valid') && !$(item).hasClassName('visited'));
  });
  return LEGAL_MOVES;
}

String.prototype.col = function() {
  return parseInt(this.slice(this.indexOf('_') + 1, this.lastIndexOf('_')));
}

String.prototype.row = function() {
  return parseInt(this.slice(this.lastIndexOf('_') + 1));
}

String.prototype.offset = function(x, y) {
  var idCol = this.col() + x;
  var idRow = this.row() + y;

  if(idCol > COLS || idRow > ROWS || idCol < 1 || idRow < 1) {
    return null;
  } else {
    return '_' + idCol + '_' + idRow;
  }
}

function generateBoard() {
  $$('.row').invoke('remove');
  ROWS.times(function(j) {
    var rowNum = j + 1;
    var row = $(document.createElement('div'));
    row.addClassName('row');
    row.writeAttribute('id', 'row' + rowNum);

    COLS.times(function(i) {
      var colNum = i + 1;
      var cell = $(document.createElement('div'));
      cell.addClassName('cell');
      cell.writeAttribute('id', '_' + colNum + '_' + rowNum);

      if(colNum == 1) cell.addClassName('first');
      if(colNum == COLS) cell.addClassName('last');
      if(rowNum == 1) cell.addClassName('bottom');
      if(rowNum == ROWS) cell.addClassName('top');

      row.appendChild(cell);
    });
    $('board').insert( { top: row } );
  });
}

function generateGame() {
  do {
    var startCell = $('_' + rand(COLS) + '_' + rand(ROWS));
  } while(legalMovesForGeneration(startCell).length == 0);

  var knight = $(document.createElement('img'));

  startCell.addClassName('visited');

  knight.writeAttribute('src', 'black_knight.png');
  knight.writeAttribute('id', 'knight');
  startCell.insert( { top: knight } );

  findNextCell(startCell, 1);
  setHandlerForLegalMoves($('knight').parentNode);
}

function findNextCell(cell, alreadyFound) {
  if( alreadyFound > (ROWS * COLS) * (5.0 / 8.0) || legalMovesForGeneration(cell).length == 0) {
    return;
  } else {
    var moves = legalMovesForGeneration(cell);
    var nextCell = $(moves[ rand(moves.length) - 1 ]);

    if(nextCell.hasClassName('valid')) {
      findNextCell(nextCell, alreadyFound);
    }
    else {
      nextCell.addClassName('valid');
      findNextCell(nextCell, alreadyFound + 1);
    }
  }
}

function saveState() {
  return {
    visited: $$('.visited'),
    current: $('knight').parentNode,
    stack: stateStack.slice()
  }
}

function restoreState(state) {
  if (!state) return;

  $$('.valid').each(function(item) { $(item).stopObserving('click') });
  $$('.visited').each(function(cell) { cell.removeClassName('visited') });
  state['visited'].each(function(cell) { cell.addClassName('visited') });
  stateStack = state['stack'];

  var knight = $('knight');
  state['current'].appendChild(knight);
  setHandlerForLegalMoves(knight.parentNode)
}

function rand(max) {
  return Math.floor(Math.random() * max + 1);
}

// Performance on this is terrible, works great up to 5x5.
function solveBoard() {
  if (isBoardFinished()) {
    console.log("We won!");
    return true;
  }

  var current = $('knight').parentNode;
  var moves = legalMoves(current);
  var solvable_move = moves.find(function(div) {
    move($(div), false);
    console.log("Moving to '"+ div +"'")
    var solvable = solveBoard();
    if (!solvable || isBoardFinished()) restoreState(stateStack.pop());
    return solvable;
  });

  if (!solvable_move) console.log("Path has no winnable moves");
  return solvable_move;
}


Add Cucumber and Bundler


Support Ruby 2.5.0

require 'rbconfig'
require 'find'
require 'ftools'

include Config

# this was adapted from rdoc's install.rb by ways of Log4r

$sitedir = CONFIG["sitelibdir"]
unless $sitedir
  version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
  $libdir = File.join(CONFIG["libdir"], "ruby", version)
  $sitedir = $:.find {|x| x =~ /site_ruby/ }
  if !$sitedir
    $sitedir = File.join($libdir, "site_ruby")
  elsif $sitedir !~ Regexp.quote(version)
    $sitedir = File.join($sitedir, version)
  end
end

makedirs = %w{ classifier }
makedirs = %w{ classifier/extensions }
makedirs = %w{ classifier/lsi }
makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}

Dir.chdir("lib")
begin
  require 'rubygems'
  require 'rake'
rescue LoadError
  puts
  puts "Please install Gem and Rake from http://rubyforge.org/projects/rubygems and http://rubyforge.org/projects/rake"
  puts
  exit(-1)
end

files = FileList["**/*"]

# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
files.each {|f|
  File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
}

begin
  require 'stemmer'
rescue LoadError
  puts
  puts "Please install Stemmer from http://rubyforge.org/projects/stemmer or via 'gem install stemmer'"
  puts
end

#--
# Copyright (c) 2005 Lucas Carlson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
# Author::    Lucas Carlson  (mailto:lucas@rufy.com)
# Copyright:: Copyright (c) 2005 Lucas Carlson
# License::   LGPL

require 'rubygems'
require 'classifier/extensions/string'
require 'classifier/bayes'
require 'classifier/lsi'

# Author::    Lucas Carlson  (mailto:lucas@rufy.com)
# Copyright:: Copyright (c) 2005 Lucas Carlson
# License::   LGPL

module Classifier

class Bayes
  # The class can be created with one or more categories, each of which will be
  # initialized and given a training method. E.g.,
  #      b = Classifier::Bayes.new 'Interesting', 'Uninteresting', 'Spam'
	def initialize(*categories)
		@categories = Hash.new
		categories.each { |category| @categories[category.prepare_category_name] = Hash.new }
		@total_words = 0
                @category_counts = Hash.new(0)
	end

	#
	# Provides a general training method for all categories specified in Bayes#new
	# For example:
	#     b = Classifier::Bayes.new 'This', 'That', 'the_other'
	#     b.train :this, "This text"
	#     b.train "that", "That text"
	#     b.train "The other", "The other text"
	def train(category, text)
		category = category.prepare_category_name
                @category_counts[category] += 1
		text.word_hash.each do |word, count|
			@categories[category][word]     ||=     0
			@categories[category][word]      +=     count
			@total_words += count
		end
	end

	#
	# Provides a untraining method for all categories specified in Bayes#new
	# Be very careful with this method.
	#
	# For example:
	#     b = Classifier::Bayes.new 'This', 'That', 'the_other'
	#     b.train :this, "This text"
	#     b.untrain :this, "This text"
	def untrain(category, text)
		category = category.prepare_category_name
                @category_counts[category] -= 1
		text.word_hash.each do |word, count|
			if @total_words >= 0
				orig = @categories[category][word]
				@categories[category][word]     ||=     0
				@categories[category][word]      -=     count
				if @categories[category][word] <= 0
					@categories[category].delete(word)
					count = orig
				end
				@total_words -= count
			end
		end
	end

	#
	# Returns the scores in each category the provided +text+. E.g.,
	#    b.classifications "I hate bad words and you"
	#    =>  {"Uninteresting"=>-12.6997928013932, "Interesting"=>-18.4206807439524}
	# The largest of these scores (the one closest to 0) is the one picked out by #classify
	def classifications(text)
		score = Hash.new
                training_count = @category_counts.values.inject { |x,y| x+y }.to_f
		@categories.each do |category, category_words|
			score[category.to_s] = 0
			total = category_words.values.inject(0) {|sum, element| sum+element}
			text.word_hash.each do |word, count|
				s = category_words.has_key?(word) ? category_words[word] : 0.1
				score[category.to_s] += Math.log(s/total.to_f)
			end
                        # now add prior probability for the category
                        s = @category_counts.has_key?(category) ? @category_counts[category] : 0.1
                        score[category.to_s] += Math.log(s / training_count)
		end
		return score
	end

  #
  # Returns the classification of the provided +text+, which is one of the
  # categories given in the initializer. E.g.,
  #    b.classify "I hate bad words and you"
  #    =>  'Uninteresting'
	def classify(text)
		(classifications(text).sort_by { |a| -a[1] })[0][0]
	end

	#
	# Provides training and untraining methods for the categories specified in Bayes#new
	# For example:
	#     b = Classifier::Bayes.new 'This', 'That', 'the_other'
	#     b.train_this "This text"
	#     b.train_that "That text"
	#     b.untrain_that "That text"
	#     b.train_the_other "The other text"
	def method_missing(name, *args)
		category = name.to_s.gsub(/(un)?train_([\w]+)/, '\2').prepare_category_name
		if @categories.has_key? category
			args.each { |text| eval("#{$1}train(category, text)") }
		elsif name.to_s =~ /(un)?train_([\w]+)/
			raise StandardError, "No such category: #{category}"
		else
	    super  #raise StandardError, "No such method: #{name}"
		end
	end

	#
	# Provides a list of category names
	# For example:
	#     b.categories
	#     =>   ['This', 'That', 'the_other']
	def categories # :nodoc:
		@categories.keys.collect {|c| c.to_s}
	end

	#
	# Allows you to add categories to the classifier.
	# For example:
	#     b.add_category "Not spam"
	#
	# WARNING: Adding categories to a trained classifier will
	# result in an undertrained category that will tend to match
	# more criteria than the trained selective categories. In short,
	# try to initialize your categories at initialization.
	def add_category(category)
		@categories[category.prepare_category_name] = Hash.new
	end

	alias append_category add_category
end

end

# Author::    Ernest Ellingson
# Copyright:: Copyright (c) 2005

# These are extensions to the std-lib 'matrix' to allow an all ruby SVD

require 'matrix'
require 'mathn'

class Array
  def sum(identity = 0, &block)
    return identity unless size > 0

    if block_given?
      map(&block).sum
    else
      reduce(:+)
    end
  end
end

class Vector
  def magnitude
    sumsqs = 0.0
    self.size.times do |i|
      sumsqs += self[i] ** 2.0
    end
    Math.sqrt(sumsqs)
  end
  def normalize
    nv = []
    mag = self.magnitude
    self.size.times do |i|

      nv << (self[i] / mag)

    end
    Vector[*nv]
  end
end

class Matrix
  def Matrix.diag(s)
     Matrix.diagonal(*s)
  end

  alias :trans :transpose

  def SV_decomp(maxSweeps = 20)
    if self.row_size >= self.column_size
      q = self.trans * self
    else
      q = self * self.trans
    end

    qrot    = q.dup
    v       = Matrix.identity(q.row_size)
    azrot   = nil
    mzrot   = nil
    cnt     = 0
    s_old   = nil
    mu      = nil

    while true do
      cnt += 1
      for row in (0...qrot.row_size-1) do
        for col in (1..qrot.row_size-1) do
          next if row == col
          h = Math.atan((2 * qrot[row,col])/(qrot[row,row]-qrot[col,col]))/2.0
          hcos = Math.cos(h)
          hsin = Math.sin(h)
          mzrot = Matrix.identity(qrot.row_size)
          mzrot[row,row] = hcos
          mzrot[row,col] = -hsin
          mzrot[col,row] = hsin
          mzrot[col,col] = hcos
          qrot = mzrot.trans * qrot * mzrot
          v = v * mzrot
        end
      end
      s_old = qrot.dup if cnt == 1
      sum_qrot = 0.0
      if cnt > 1
        qrot.row_size.times do |r|
          sum_qrot += (qrot[r,r]-s_old[r,r]).abs if (qrot[r,r]-s_old[r,r]).abs > 0.001
        end
        s_old = qrot.dup
      end
      break if (sum_qrot <= 0.001 and cnt > 1) or cnt >= maxSweeps
    end # of do while true
    s = []
    qrot.row_size.times do |r|
      s << Math.sqrt(qrot[r,r])
    end
    #puts "cnt = #{cnt}"
    if self.row_size >= self.column_size
      mu = self *  v * Matrix.diagonal(*s).inverse
      return [mu, v, s]
    else
      puts v.row_size
      puts v.column_size
      puts self.row_size
      puts self.column_size
      puts s.size

      mu = (self.trans * v *  Matrix.diagonal(*s).inverse)
      return [mu, v, s]
    end
  end
  def []=(i,j,val)
    @rows[i][j] = val
  end
end

module GSL

  class Vector
    def _dump(v)
      Marshal.dump( self.to_a )
    end

    def self._load(arr)
      arry = Marshal.load(arr)
      return GSL::Vector.alloc(arry)
    end

  end

  class Matrix
     class <<self
        alias :diag :diagonal
     end
  end
end

# Author::    Lucas Carlson  (mailto:lucas@rufy.com)
# Copyright:: Copyright (c) 2005 Lucas Carlson
# License::   LGPL

require "set"

# These are extensions to the String class to provide convenience
# methods for the Classifier package.
class String

  # Removes common punctuation symbols, returning a new string.
  # E.g.,
  #   "Hello (greeting's), with {braces} < >...?".without_punctuation
  #   => "Hello  greetings   with  braces         "
  def without_punctuation
    tr( ',?.!;:"@#$%^&*()_=+[]{}\|<>/`~', " " ) .tr( "'\-", "")
  end

  # Return a Hash of strings => ints. Each word in the string is stemmed,
  # interned, and indexes to its frequency in the document.
	def word_hash
		word_hash = clean_word_hash()
		symbol_hash = word_hash_for_symbols(gsub(/[\w]/," ").split)
		return word_hash.merge(symbol_hash)
	end

	# Return a word hash without extra punctuation or short symbols, just stemmed words
	def clean_word_hash
		word_hash_for_words gsub(/[^\w\s]/,"").split
	end

	private

	def word_hash_for_words(words)
		d = Hash.new(0)
		words.each do |word|
			word.downcase!
			if ! CORPUS_SKIP_WORDS.include?(word) && word.length > 2
				d[word.stem.intern] += 1
			end
		end
		return d
	end

	def word_hash_for_symbols(words)
		d = Hash.new(0)
		words.each do |word|
			d[word.intern] += 1
		end
		return d
	end

	CORPUS_SKIP_WORDS = Set.new([
      "a",
      "again",
      "all",
      "along",
      "are",
      "also",
      "an",
      "and",
      "as",
      "at",
      "but",
      "by",
      "came",
      "can",
      "cant",
      "couldnt",
      "did",
      "didn",
      "didnt",
      "do",
      "doesnt",
      "dont",
      "ever",
      "first",
      "from",
      "have",
      "her",
      "here",
      "him",
      "how",
      "i",
      "if",
      "in",
      "into",
      "is",
      "isnt",
      "it",
      "itll",
      "just",
      "last",
      "least",
      "like",
      "most",
      "my",
      "new",
      "no",
      "not",
      "now",
      "of",
      "on",
      "or",
      "should",
      "sinc",
      "so",
      "some",
      "th",
      "than",
      "this",
      "that",
      "the",
      "their",
      "then",
      "those",
      "to",
      "told",
      "too",
      "true",
      "try",
      "until",
      "url",
      "us",
      "were",
      "when",
      "whether",
      "while",
      "with",
      "within",
      "yes",
      "you",
      "youll",
      ])
end

# Author::    David Fayram  (mailto:dfayram@lensmen.net)
# Copyright:: Copyright (c) 2005 David Fayram II
# License::   LGPL

begin
   raise LoadError if ENV['NATIVE_VECTOR'] == "true" # to test the native vector class, try `rake test NATIVE_VECTOR=true`

   require 'gsl' # requires http://rb-gsl.rubyforge.org/
   require 'classifier/extensions/vector_serialize'
   $GSL = true

rescue LoadError
	warn "Notice: for 10x faster LSI support, please install http://rb-gsl.rubyforge.org/"
	require 'classifier/extensions/vector'
end

require 'classifier/lsi/word_list'
require 'classifier/lsi/content_node'
require 'classifier/lsi/summary'

module Classifier

  # This class implements a Latent Semantic Indexer, which can search, classify and cluster
  # data based on underlying semantic relations. For more information on the algorithms used,
  # please consult Wikipedia[http://en.wikipedia.org/wiki/Latent_Semantic_Indexing].
  class LSI

    attr_reader :word_list
    attr_accessor :auto_rebuild

    # Create a fresh index.
    # If you want to call #build_index manually, use
    #      Classifier::LSI.new :auto_rebuild => false
    #
    def initialize(options = {})
      @auto_rebuild = true unless options[:auto_rebuild] == false
      @word_list, @items = WordList.new, {}
      @version, @built_at_version = 0, -1
    end

    # Returns true if the index needs to be rebuilt.  The index needs
    # to be built after all informaton is added, but before you start
    # using it for search, classification and cluster detection.
    def needs_rebuild?
      (@items.keys.size > 1) && (@version != @built_at_version)
    end

    # Adds an item to the index. item is assumed to be a string, but
    # any item may be indexed so long as it responds to #to_s or if
    # you provide an optional block explaining how the indexer can
    # fetch fresh string data. This optional block is passed the item,
    # so the item may only be a reference to a URL or file name.
    #
    # For example:
    #   lsi = Classifier::LSI.new
    #   lsi.add_item "This is just plain text"
    #   lsi.add_item "/home/me/filename.txt" { |x| File.read x }
    #   ar = ActiveRecordObject.find( :all )
    #   lsi.add_item ar, *ar.categories { |x| ar.content }
    #
    def add_item( item, *categories, &block )
      clean_word_hash = block ? block.call(item).clean_word_hash : item.to_s.clean_word_hash
      @items[item] = ContentNode.new(clean_word_hash, *categories)
      @version += 1
      build_index if @auto_rebuild
    end

    # A less flexible shorthand for add_item that assumes
    # you are passing in a string with no categorries. item
    # will be duck typed via to_s .
    #
    def <<( item )
      add_item item
    end

    # Returns the categories for a given indexed items. You are free to add and remove
    # items from this as you see fit. It does not invalide an index to change its categories.
    def categories_for(item)
      return [] unless @items[item]
      return @items[item].categories
    end

    # Removes an item from the database, if it is indexed.
    #
    def remove_item( item )
      if @items.keys.contain? item
        @items.remove item
        @version += 1
      end
    end

    # Returns an array of items that are indexed.
    def items
      @items.keys
    end

    # Returns the categories for a given indexed items. You are free to add and remove
    # items from this as you see fit. It does not invalide an index to change its categories.
    def categories_for(item)
      return [] unless @items[item]
      return @items[item].categories
    end

    # This function rebuilds the index if needs_rebuild? returns true.
    # For very large document spaces, this indexing operation may take some
    # time to complete, so it may be wise to place the operation in another
    # thread.
    #
    # As a rule, indexing will be fairly swift on modern machines until
    # you have well over 500 documents indexed, or have an incredibly diverse
    # vocabulary for your documents.
    #
    # The optional parameter "cutoff" is a tuning parameter. When the index is
    # built, a certain number of s-values are discarded from the system. The
    # cutoff parameter tells the indexer how many of these values to keep.
    # A value of 1 for cutoff means that no semantic analysis will take place,
    # turning the LSI class into a simple vector search engine.
    def build_index( cutoff=0.75 )
      return unless needs_rebuild?
      make_word_list

      doc_list = @items.values
      tda = doc_list.collect { |node| node.raw_vector_with( @word_list ) }

      if $GSL
         tdm = GSL::Matrix.alloc(*tda).trans
         ntdm = build_reduced_matrix(tdm, cutoff)

         ntdm.size[1].times do |col|
           vec = GSL::Vector.alloc( ntdm.column(col) ).row
           doc_list[col].lsi_vector = vec
           doc_list[col].lsi_norm = vec.normalize
         end
      else
         tdm = Matrix.rows(tda).trans
         ntdm = build_reduced_matrix(tdm, cutoff)

         ntdm.row_size.times do |col|
           doc_list[col].lsi_vector = ntdm.column(col) if doc_list[col]
           doc_list[col].lsi_norm = ntdm.column(col).normalize  if doc_list[col]
         end
      end

      @built_at_version = @version
    end

    # This method returns max_chunks entries, ordered by their average semantic rating.
    # Essentially, the average distance of each entry from all other entries is calculated,
    # the highest are returned.
    #
    # This can be used to build a summary service, or to provide more information about
    # your dataset's general content. For example, if you were to use categorize on the
    # results of this data, you could gather information on what your dataset is generally
    # about.
    def highest_relative_content( max_chunks=10 )
       return [] if needs_rebuild?

       avg_density = Hash.new
       @items.each_key { |x| avg_density[x] = proximity_array_for_content(x).inject(0.0) { |x,y| x + y[1]} }

       avg_density.keys.sort_by { |x| avg_density[x] }.reverse[0..max_chunks-1].map
    end

    # This function is the primitive that find_related and classify
    # build upon. It returns an array of 2-element arrays. The first element
    # of this array is a document, and the second is its "score", defining
    # how "close" it is to other indexed items.
    #
    # These values are somewhat arbitrary, having to do with the vector space
    # created by your content, so the magnitude is interpretable but not always
    # meaningful between indexes.
    #
    # The parameter doc is the content to compare. If that content is not
    # indexed, you can pass an optional block to define how to create the
    # text data. See add_item for examples of how this works.
    def proximity_array_for_content( doc, &block )
      return [] if needs_rebuild?

      content_node = node_for_content( doc, &block )
      result =
        @items.keys.collect do |item|
          if $GSL
             val = content_node.search_vector * @items[item].search_vector.col
          else
             val = (Matrix[content_node.search_vector] * @items[item].search_vector)[0]
          end
          [item, val]
        end
      result.sort_by { |x| x[1] }.reverse
    end

    # Similar to proximity_array_for_content, this function takes similar
    # arguments and returns a similar array. However, it uses the normalized
    # calculated vectors instead of their full versions. This is useful when
    # you're trying to perform operations on content that is much smaller than
    # the text you're working with. search uses this primitive.
    def proximity_norms_for_content( doc, &block )
      return [] if needs_rebuild?

      content_node = node_for_content( doc, &block )
      result =
        @items.keys.collect do |item|
          if $GSL
            val = content_node.search_norm * @items[item].search_norm.col
          else
            val = (Matrix[content_node.search_norm] * @items[item].search_norm)[0]
          end
          [item, val]
        end
      result.sort_by { |x| x[1] }.reverse
    end

    # This function allows for text-based search of your index. Unlike other functions
    # like find_related and classify, search only takes short strings. It will also ignore
    # factors like repeated words. It is best for short, google-like search terms.
    # A search will first priortize lexical relationships, then semantic ones.
    #
    # While this may seem backwards compared to the other functions that LSI supports,
    # it is actually the same algorithm, just applied on a smaller document.
    def search( string, max_nearest=3 )
      return [] if needs_rebuild?
      carry = proximity_norms_for_content( string )
      result = carry.collect { |x| x[0] }
      return result[0..max_nearest-1]
    end

    # This function takes content and finds other documents
    # that are semantically "close", returning an array of documents sorted
    # from most to least relavant.
    # max_nearest specifies the number of documents to return. A value of
    # 0 means that it returns all the indexed documents, sorted by relavence.
    #
    # This is particularly useful for identifing clusters in your document space.
    # For example you may want to identify several "What's Related" items for weblog
    # articles, or find paragraphs that relate to each other in an essay.
    def find_related( doc, max_nearest=3, &block )
      carry =
        proximity_array_for_content( doc, &block ).reject { |pair| pair[0] == doc }
      result = carry.collect { |x| x[0] }
      return result[0..max_nearest-1]
    end

    # This function uses a voting system to categorize documents, based on
    # the categories of other documents. It uses the same logic as the
    # find_related function to find related documents, then returns the
    # most obvious category from this list.
    #
    # cutoff signifies the number of documents to consider when clasifying
    # text. A cutoff of 1 means that every document in the index votes on
    # what category the document is in. This may not always make sense.
    #
    def classify( doc, cutoff=0.30, &block )
      icutoff = (@items.size * cutoff).round
      carry = proximity_array_for_content( doc, &block )
      carry = carry[0..icutoff-1]
      votes = {}
      carry.each do |pair|
        categories = @items[pair[0]].categories
        categories.each do |category|
          votes[category] ||= 0.0
          votes[category] += pair[1]
        end
      end

      ranking = votes.keys.sort_by { |x| votes[x] }
      return ranking[-1]
    end

    # Prototype, only works on indexed documents.
    # I have no clue if this is going to work, but in theory
    # it's supposed to.
    def highest_ranked_stems( doc, count=3 )
      raise "Requested stem ranking on non-indexed content!" unless @items[doc]
      arr = node_for_content(doc).lsi_vector.to_a
      top_n = arr.sort.reverse[0..count-1]
      return top_n.collect { |x| @word_list.word_for_index(arr.index(x))}
    end

    private
    def build_reduced_matrix( matrix, cutoff=0.75 )
      # TODO: Check that M>=N on these dimensions! Transpose helps assure this
      u, v, s = matrix.SV_decomp

      # TODO: Better than 75% term, please. :\
      s_cutoff = s.sort.reverse[(s.size * cutoff).round - 1]
      s.size.times do |ord|
        s[ord] = 0.0 if s[ord] < s_cutoff
      end
      # Reconstruct the term document matrix, only with reduced rank
      u * ($GSL ? GSL::Matrix : ::Matrix).diag( s ) * v.trans
    end

    def node_for_content(item, &block)
      if @items[item]
        return @items[item]
      else
        clean_word_hash = block ? block.call(item).clean_word_hash : item.to_s.clean_word_hash

        cn = ContentNode.new(clean_word_hash, &block) # make the node and extract the data

        unless needs_rebuild?
          cn.raw_vector_with( @word_list ) # make the lsi raw and norm vectors
        end
      end

      return cn
    end

    def make_word_list
      @word_list = WordList.new
      @items.each_value do |node|
        node.word_hash.each_key { |key| @word_list.add_word key }
      end
    end

  end
end

# Author::    David Fayram  (mailto:dfayram@lensmen.net)
# Copyright:: Copyright (c) 2005 David Fayram II
# License::   LGPL

module Classifier

# This is an internal data structure class for the LSI node. Save for
# raw_vector_with, it should be fairly straightforward to understand.
# You should never have to use it directly.
  class ContentNode
    attr_accessor :raw_vector, :raw_norm,
                  :lsi_vector, :lsi_norm,
                  :categories

    attr_reader :word_hash
    # If text_proc is not specified, the source will be duck-typed
    # via source.to_s
    def initialize( word_hash, *categories )
      @categories = categories || []
      @word_hash = word_hash
    end

    # Use this to fetch the appropriate search vector.
    def search_vector
      @lsi_vector || @raw_vector
    end

    # Use this to fetch the appropriate search vector in normalized form.
    def search_norm
      @lsi_norm || @raw_norm
    end

    # Creates the raw vector out of word_hash using word_list as the
    # key for mapping the vector space.
    def raw_vector_with( word_list )
      if $GSL
         vec = GSL::Vector.alloc(word_list.size)
      else
         vec = Array.new(word_list.size, 0)
      end

      @word_hash.each_key do |word|
        vec[word_list[word]] = @word_hash[word] if word_list[word]
      end

      # Perform the scaling transform
      total_words = vec.sum

      # Perform first-order association transform if this vector has more
      # than one word in it.
      if total_words > 1.0
        weighted_total = 0.0
        vec.each do |term|
          if ( term > 0 )
            weighted_total += (( term / total_words ) * Math.log( term / total_words ))
          end
        end
        vec = vec.collect { |val| Math.log( val + 1 ) / -weighted_total }
      end

      if $GSL
         @raw_norm   = vec.normalize
         @raw_vector = vec
      else
         @raw_norm   = Vector[*vec].normalize
         @raw_vector = Vector[*vec]
      end
    end

  end

end

# Author::    Lucas Carlson  (mailto:lucas@rufy.com)
# Copyright:: Copyright (c) 2005 Lucas Carlson
# License::   LGPL

class String
   def summary( count=10, separator=" [...] " )
      perform_lsi split_sentences, count, separator
   end

   def paragraph_summary( count=1, separator=" [...] " )
      perform_lsi split_paragraphs, count, separator
   end

   def split_sentences
      split /(\.|\!|\?)/ # TODO: make this less primitive
   end

   def split_paragraphs
      split /(\n\n|\r\r|\r\n\r\n)/ # TODO: make this less primitive
   end

   private

   def perform_lsi(chunks, count, separator)
      lsi = Classifier::LSI.new :auto_rebuild => false
      chunks.each { |chunk| lsi << chunk unless chunk.strip.empty? || chunk.strip.split.size == 1 }
      lsi.build_index
      summaries = lsi.highest_relative_content count
      return summaries.reject { |chunk| !summaries.include? chunk }.map { |x| x.strip }.join(separator)
   end
end

# Author::    David Fayram  (mailto:dfayram@lensmen.net)
# Copyright:: Copyright (c) 2005 David Fayram II
# License::   LGPL

module Classifier
  # This class keeps a word => index mapping. It is used to map stemmed words
  # to dimensions of a vector.

  class WordList
    def initialize
      @location_table = Hash.new
    end

    # Adds a word (if it is new) and assigns it a unique dimension.
    def add_word(word)
      term = word
      @location_table[term] = @location_table.size unless @location_table[term]
    end

    # Returns the dimension of the word or nil if the word is not in the space.
    def [](lookup)
      term = lookup
      @location_table[term]
    end

    def word_for_index(ind)
      @location_table.invert[ind]
    end

    # Returns the number of words mapped.
    def size
      @location_table.size
    end

  end
end

require File.dirname(__FILE__) + '/../test_helper'
class BayesianTest < Test::Unit::TestCase
	def setup
		@classifier = Classifier::Bayes.new 'Interesting', 'Uninteresting'
	end

	def test_good_training
		assert_nothing_raised { @classifier.train_interesting "love" }
	end

	def test_bad_training
		assert_raise(StandardError) { @classifier.train_no_category "words" }
	end

	def test_bad_method
		assert_raise(NoMethodError) { @classifier.forget_everything_you_know "" }
	end

	def test_categories
		assert_equal ['Interesting', 'Uninteresting'].sort, @classifier.categories.sort
	end

	def test_add_category
		@classifier.add_category 'Test'
		assert_equal ['Test', 'Interesting', 'Uninteresting'].sort, @classifier.categories.sort
	end

	def test_classification
		@classifier.train_interesting "here are some good words. I hope you love them"
		@classifier.train_uninteresting "here are some bad words, I hate you"
		assert_equal 'Uninteresting', @classifier.classify("I hate bad words and you")
	end
end

require File.dirname(__FILE__) + '/../test_helper'
class StringExtensionsTest < Test::Unit::TestCase
	def test_word_hash
		hash = {:good=>1, :"!"=>1, :hope=>1, :"'"=>1, :"."=>1, :love=>1, :word=>1, :them=>1, :test=>1}
		assert_equal hash, "here are some good words of test's. I hope you love them!".word_hash
	end

	def test_clean_word_hash
	   hash = {:good=>1, :word=>1, :hope=>1, :love=>1, :them=>1, :test=>1}
	   assert_equal hash, "here are some good words of test's. I hope you love them!".clean_word_hash
	end

end

class ArrayExtensionsTest < Test::Unit::TestCase

  def test_plays_nicely_with_any_array
    assert_equal [Array].sum, Array
  end

  def test_monkey_path_array_sum
    assert_equal [1,2,3].sum, 6
  end

  def test_summing_an_empty_array
    assert_equal [nil].sum, 0
  end

  def test_summing_an_empty_array
    assert_equal Array[].sum, 0
  end

end

require File.dirname(__FILE__) + '/../test_helper'
class LSITest < Test::Unit::TestCase
	def setup
	  # we repeat principle words to help weight them.
	  # This test is rather delicate, since this system is mostly noise.
     @str1 = "This text deals with dogs. Dogs."
	  @str2 = "This text involves dogs too. Dogs! "
	  @str3 = "This text revolves around cats. Cats."
	  @str4 = "This text also involves cats. Cats!"
	  @str5 = "This text involves birds. Birds."
	end

	def test_basic_indexing
	 lsi = Classifier::LSI.new
	  [@str1, @str2, @str3, @str4, @str5].each { |x| lsi << x }
	  assert ! lsi.needs_rebuild?

	 # note that the closest match to str1 is str2, even though it is not
	 # the closest text match.
	 assert_equal [@str2, @str5, @str3], lsi.find_related(@str1, 3)
	end

	def test_not_auto_rebuild
	 lsi = Classifier::LSI.new :auto_rebuild => false
	 lsi.add_item @str1, "Dog"
	 lsi.add_item @str2, "Dog"
	 assert lsi.needs_rebuild?
	 lsi.build_index
	 assert ! lsi.needs_rebuild?
	end

	def test_basic_categorizing
	  lsi = Classifier::LSI.new
	  lsi.add_item @str2, "Dog"
	  lsi.add_item @str3, "Cat"
	  lsi.add_item @str4, "Cat"
	  lsi.add_item @str5, "Bird"

	  assert_equal "Dog", lsi.classify( @str1 )
	  assert_equal "Cat", lsi.classify( @str3 )
     assert_equal "Bird", lsi.classify( @str5 )
	end

	def test_external_classifying
	  lsi = Classifier::LSI.new
	  bayes = Classifier::Bayes.new 'Dog', 'Cat', 'Bird'
	  lsi.add_item @str1, "Dog" ; bayes.train_dog @str1
	  lsi.add_item @str2, "Dog" ; bayes.train_dog @str2
	  lsi.add_item @str3, "Cat" ; bayes.train_cat @str3
	  lsi.add_item @str4, "Cat" ; bayes.train_cat @str4
	  lsi.add_item @str5, "Bird" ; bayes.train_bird @str5

	  # We're talking about dogs. Even though the text matches the corpus on
	  # cats better.  Dogs have more semantic weight than cats. So bayes
	  # will fail here, but the LSI recognizes content.
	  tricky_case = "This text revolves around dogs."
	  assert_equal "Dog", lsi.classify( tricky_case )
	  assert_not_equal "Dog", bayes.classify( tricky_case )
	end

	def test_recategorize_interface
	  lsi = Classifier::LSI.new
	  lsi.add_item @str1, "Dog"
	  lsi.add_item @str2, "Dog"
	  lsi.add_item @str3, "Cat"
	  lsi.add_item @str4, "Cat"
	  lsi.add_item @str5, "Bird"

	  tricky_case = "This text revolves around dogs."
	  assert_equal "Dog", lsi.classify( tricky_case )

	  # Recategorize as needed.
	  lsi.categories_for(@str1).clear.push "Cow"
	  lsi.categories_for(@str2).clear.push "Cow"

	  assert !lsi.needs_rebuild?
	  assert_equal "Cow", lsi.classify( tricky_case )
	end

	def test_search
	  lsi = Classifier::LSI.new
	  [@str1, @str2, @str3, @str4, @str5].each { |x| lsi << x }

	  # Searching by content and text, note that @str2 comes up first, because
	  # both "dog" and "involve" are present. But, the next match is @str1 instead
	  # of @str4, because "dog" carries more weight than involves.
	  assert_equal( [@str2, @str1, @str4, @str5, @str3],
	                lsi.search("dog involves", 100) )

	  # Keyword search shows how the space is mapped out in relation to
	  # dog when magnitude is remove. Note the relations. We move from dog
	  # through involve and then finally to other words.
	  assert_equal( [@str1, @str2, @str4, @str5, @str3],
	                lsi.search("dog", 5) )
	end

	def test_serialize_safe
    lsi = Classifier::LSI.new
	  [@str1, @str2, @str3, @str4, @str5].each { |x| lsi << x }

	  lsi_md = Marshal.dump lsi
	  lsi_m = Marshal.load lsi_md

	  assert_equal lsi_m.search("cat", 3), lsi.search("cat", 3)
	  assert_equal lsi_m.find_related(@str1, 3), lsi.find_related(@str1, 3)
	end

	def test_keyword_search
	  lsi = Classifier::LSI.new
	  lsi.add_item @str1, "Dog"
	  lsi.add_item @str2, "Dog"
	  lsi.add_item @str3, "Cat"
	  lsi.add_item @str4, "Cat"
	  lsi.add_item @str5, "Bird"

	  assert_equal [:dog, :text, :deal], lsi.highest_ranked_stems(@str1)
	end

	def test_summary
	   assert_equal "This text involves dogs too [...] This text also involves cats", [@str1, @str2, @str3, @str4, @str5].join.summary(2)
	end

end

$:.unshift(File.dirname(__FILE__) + '/../lib')

require 'test/unit'
require 'classifier'

require File.dirname(__FILE__) + '/../test_helper'
class BayesianTest < Minitest::Test
	def setup
		@classifier = Classifier::Bayes.new 'Interesting', 'Uninteresting'
	end

	def test_good_training
		assert_nothing_raised { @classifier.train_interesting "love" }
	end

	def test_bad_training
		assert_raise(StandardError) { @classifier.train_no_category "words" }
	end

	def test_bad_method
		assert_raise(NoMethodError) { @classifier.forget_everything_you_know "" }
	end

	def test_categories
		assert_equal ['Interesting', 'Uninteresting'].sort, @classifier.categories.sort
	end

	def test_add_category
		@classifier.add_category 'Test'
		assert_equal ['Test', 'Interesting', 'Uninteresting'].sort, @classifier.categories.sort
	end

	def test_classification
		@classifier.train_interesting "here are some good words. I hope you love them"
		@classifier.train_uninteresting "here are some bad words, I hate you"
		assert_equal 'Uninteresting', @classifier.classify("I hate bad words and you")
	end
end

require File.dirname(__FILE__) + '/../test_helper'
class StringExtensionsTest < Minitest::Test
	def test_word_hash
		hash = {:good=>1, :"!"=>1, :hope=>1, :"'"=>1, :"."=>1, :love=>1, :word=>1, :them=>1, :test=>1}
		assert_equal hash, "here are some good words of test's. I hope you love them!".word_hash
	end

	def test_clean_word_hash
	   hash = {:good=>1, :word=>1, :hope=>1, :love=>1, :them=>1, :test=>1}
	   assert_equal hash, "here are some good words of test's. I hope you love them!".clean_word_hash
	end

end

class ArrayExtensionsTest < Minitest::Test

  def test_plays_nicely_with_any_array
    assert_equal [Array].sum, Array
  end

  def test_monkey_path_array_sum
    assert_equal [1,2,3].sum, 6
  end

  def test_summing_an_empty_array
    assert_equal [nil].sum, 0
  end

  def test_summing_an_empty_array
    assert_equal Array[].sum, 0
  end

end

require File.dirname(__FILE__) + '/../test_helper'
class LSITest < Minitest::Test
	def setup
	  # we repeat principle words to help weight them.
	  # This test is rather delicate, since this system is mostly noise.
     @str1 = "This text deals with dogs. Dogs."
	  @str2 = "This text involves dogs too. Dogs! "
	  @str3 = "This text revolves around cats. Cats."
	  @str4 = "This text also involves cats. Cats!"
	  @str5 = "This text involves birds. Birds."
	end

	def test_basic_indexing
	 lsi = Classifier::LSI.new
	  [@str1, @str2, @str3, @str4, @str5].each { |x| lsi << x }
	  assert ! lsi.needs_rebuild?

	 # note that the closest match to str1 is str2, even though it is not
	 # the closest text match.
	 assert_equal [@str2, @str5, @str3], lsi.find_related(@str1, 3)
	end

	def test_not_auto_rebuild
	 lsi = Classifier::LSI.new :auto_rebuild => false
	 lsi.add_item @str1, "Dog"
	 lsi.add_item @str2, "Dog"
	 assert lsi.needs_rebuild?
	 lsi.build_index
	 assert ! lsi.needs_rebuild?
	end

	def test_basic_categorizing
	  lsi = Classifier::LSI.new
	  lsi.add_item @str2, "Dog"
	  lsi.add_item @str3, "Cat"
	  lsi.add_item @str4, "Cat"
	  lsi.add_item @str5, "Bird"

	  assert_equal "Dog", lsi.classify( @str1 )
	  assert_equal "Cat", lsi.classify( @str3 )
     assert_equal "Bird", lsi.classify( @str5 )
	end

	def test_external_classifying
	  lsi = Classifier::LSI.new
	  bayes = Classifier::Bayes.new 'Dog', 'Cat', 'Bird'
	  lsi.add_item @str1, "Dog" ; bayes.train_dog @str1
	  lsi.add_item @str2, "Dog" ; bayes.train_dog @str2
	  lsi.add_item @str3, "Cat" ; bayes.train_cat @str3
	  lsi.add_item @str4, "Cat" ; bayes.train_cat @str4
	  lsi.add_item @str5, "Bird" ; bayes.train_bird @str5

	  # We're talking about dogs. Even though the text matches the corpus on
	  # cats better.  Dogs have more semantic weight than cats. So bayes
	  # will fail here, but the LSI recognizes content.
	  tricky_case = "This text revolves around dogs."
	  assert_equal "Dog", lsi.classify( tricky_case )
	  assert_not_equal "Dog", bayes.classify( tricky_case )
	end

	def test_recategorize_interface
	  lsi = Classifier::LSI.new
	  lsi.add_item @str1, "Dog"
	  lsi.add_item @str2, "Dog"
	  lsi.add_item @str3, "Cat"
	  lsi.add_item @str4, "Cat"
	  lsi.add_item @str5, "Bird"

	  tricky_case = "This text revolves around dogs."
	  assert_equal "Dog", lsi.classify( tricky_case )

	  # Recategorize as needed.
	  lsi.categories_for(@str1).clear.push "Cow"
	  lsi.categories_for(@str2).clear.push "Cow"

	  assert !lsi.needs_rebuild?
	  assert_equal "Cow", lsi.classify( tricky_case )
	end

	def test_search
	  lsi = Classifier::LSI.new
	  [@str1, @str2, @str3, @str4, @str5].each { |x| lsi << x }

	  # Searching by content and text, note that @str2 comes up first, because
	  # both "dog" and "involve" are present. But, the next match is @str1 instead
	  # of @str4, because "dog" carries more weight than involves.
	  assert_equal( [@str2, @str1, @str4, @str5, @str3],
	                lsi.search("dog involves", 100) )

	  # Keyword search shows how the space is mapped out in relation to
	  # dog when magnitude is remove. Note the relations. We move from dog
	  # through involve and then finally to other words.
	  assert_equal( [@str1, @str2, @str4, @str5, @str3],
	                lsi.search("dog", 5) )
	end

	def test_serialize_safe
    lsi = Classifier::LSI.new
	  [@str1, @str2, @str3, @str4, @str5].each { |x| lsi << x }

	  lsi_md = Marshal.dump lsi
	  lsi_m = Marshal.load lsi_md

	  assert_equal lsi_m.search("cat", 3), lsi.search("cat", 3)
	  assert_equal lsi_m.find_related(@str1, 3), lsi.find_related(@str1, 3)
	end

	def test_keyword_search
	  lsi = Classifier::LSI.new
	  lsi.add_item @str1, "Dog"
	  lsi.add_item @str2, "Dog"
	  lsi.add_item @str3, "Cat"
	  lsi.add_item @str4, "Cat"
	  lsi.add_item @str5, "Bird"

	  assert_equal [:dog, :text, :deal], lsi.highest_ranked_stems(@str1)
	end

	def test_summary
	   assert_equal "This text involves dogs too [...] This text also involves cats", [@str1, @str2, @str3, @str4, @str5].join.summary(2)
	end

end

$:.unshift(File.dirname(__FILE__) + '/../lib')

require 'minitest'
require 'classifier'


Don't sign the gem during test build.

# frozen_string_literal: true

source 'https://rubygems.org'

gemspec

group :maintenance, optional: true do
	gem "bake-gem"
	gem "bake-modernize"

	gem "utopia-project"
end

group :development do
	gem 'json'
	gem 'rackula'
end

group :test do
	gem 'benchmark-ips'
	gem 'ruby-prof', platforms: :mri

	gem 'guard-falcon'

	gem 'rack-test'
end

gem "thread-local", "~> 1.0"

# frozen_string_literal: true

# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'fileutils'
require 'tmpdir'
require 'yaml'

require 'open3'
require 'bundler'

RSpec.describe "utopia command" do
	let(:utopia_path) {File.expand_path("../..", __dir__)}
	let(:pkg_path) {File.expand_path("pkg", utopia_path)}
	let(:utopia) {File.expand_path("../../bin/utopia", __dir__)}

	before(:all) do
		# We need to build a package to test deployment:
		system("bundle", "exec", "bake", "gem:build") or abort("Could not build package for setup spec!")
	end

	around(:each) do |example|
		Bundler.with_unbundled_env do
			# If we don't delete this, when running on travis, it will try submit the coverage report.
			ENV.delete('COVERAGE')

			# In order to make this work in a vendored test environment, we need to seed the "local" environment with the "utopia" gem and all it's dependencies. Otherwise, the commands will fail because they can't find the dependencies (they are vendored in the source root but not in `dir`):
			system("bundle", "install", "--system")

			example.run
		end
	end

	def sh_status(*arguments)
		system(*arguments) ? 0 : false
	end

	def sh_stdout(*arguments)
		output, status = Open3.capture2(*arguments)
		return output
	end

	def git_config(name, value=nil)
		unless value.nil?
			return sh_status('git', 'config', name, value)
		else
			return sh_stdout('git', 'config', name).chomp
		end
	end

	def group_rw(path)
		gaccess = File.stat(path).mode.to_s(8)[-2]
		return gaccess == '6' || gaccess == '7'
	end

	def install_packages(dir)
		system("bundle", "config", "--local", "local.utopia", utopia_path, chdir: dir)
		system("bundle", "config", "--local", "cache_path", pkg_path, chdir: dir)
	end

	it "should generate sample site" do
		Dir.mktmpdir('test-site') do |dir|
			install_packages(dir)

			system(utopia, "--in", dir, "site", "create")

			expect(Dir.entries(dir)).to include(".git", "gems.rb", "gems.locked", "README.md", "bake.rb", "config.ru", "lib", "pages", "public", "spec")

			expect(
				system("bundle", "exec", "bake", "utopia:test", chdir: dir)
			).to be true
		end
	end

	it "should generate a sample server" do
		Dir.mktmpdir('test-server') do |dir|
			install_packages(dir)

			result = sh_status(utopia, "--in", dir, "server", "create")
			expect(result).to be == 0

			expect(Dir.entries(dir)).to include(".git")

			# make sure git is set up properly
			Dir.chdir(dir) do
				expect(git_config 'core.sharedRepository').to be == '1'
				expect(git_config 'receive.denyCurrentBranch').to be == 'ignore'
				expect(git_config 'core.worktree').to be == dir
			end

			environment = YAML.load_file(File.join(dir, 'config/environment.yaml'))
			expect(environment).to include('VARIANT', 'UTOPIA_SESSION_SECRET')
		end
	end

	it "should not trash the sample server during update" do
		Dir.mktmpdir('test-server') do |dir|
			install_packages(dir)

			result = sh_status(utopia, "--in", dir, "server", "create")
			expect(result).to be == 0

			Dir.chdir(dir) do
				# make the repository look a bit like like it's an old one
				git_config 'core.sharedRepository', 'false'
				sh_status 'chmod', '-Rf', 'g-x', '.git'
				sh_status 'rm', '-f', '.git/hooks/post-receive'
			end

			result = sh_status(utopia, "--in", dir, "server", "update")
			expect(result).to be == 0

			# check a couple of files to make sure they have group read and write access
			# after the update
			Dir.glob(File.join(dir, '.git/**/*')).each do |path|
				expect(group_rw path).to be == true
			end
		end
	end

	it "can generate sample site, server and push to the server" do
		# This assumes your default branch is "main". We should probably be more flexible around this.
		# git config --global init.defaultBranch main
		Dir.mktmpdir('test') do |dir|
			site_path = File.join(dir, 'site')

			install_packages(site_path)

			server_path = File.join(dir, 'server')

			result = sh_status(utopia, "--in", site_path, "site", "create")
			expect(result).to be == 0

			result = sh_status(utopia, "--in", server_path, "server", "create")
			expect(result).to be == 0

			Dir.chdir(site_path) do
				result = sh_status("git", "push", "--set-upstream", server_path, "main")
				expect(result).to be == 0
			end

			files = %W[.git gems.rb gems.locked README.md bake.rb config.ru lib pages public]

			expect(Dir.entries(server_path)).to include(*files)

			expect(File.executable? File.join(server_path, 'config.ru')).to be == true

			puts File.stat(File.join(dir, 'server', '.git')).mode
		end
	end
end

# frozen_string_literal: true

source 'https://rubygems.org'

gemspec

group :maintenance, optional: true do
	gem "bake-gem", "~> 0.3"
	gem "bake-modernize"

	gem "utopia-project"
end

group :development do
	gem 'json'
	gem 'rackula'
end

group :test do
	gem 'benchmark-ips'
	gem 'ruby-prof', platforms: :mri

	gem 'guard-falcon'

	gem 'rack-test'
end

gem "thread-local", "~> 1.0"

# frozen_string_literal: true

# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'fileutils'
require 'tmpdir'
require 'yaml'

require 'open3'
require 'bundler'

RSpec.describe "utopia command" do
	let(:utopia_path) {File.expand_path("../..", __dir__)}
	let(:pkg_path) {File.expand_path("pkg", utopia_path)}
	let(:utopia) {File.expand_path("../../bin/utopia", __dir__)}

	before(:all) do
		# We need to build a package to test deployment:
		system("bundle", "exec", "bake", "gem:build", "--signing-key", "no") or abort("Could not build package for setup spec!")
	end

	around(:each) do |example|
		Bundler.with_unbundled_env do
			# If we don't delete this, when running on travis, it will try submit the coverage report.
			ENV.delete('COVERAGE')

			# In order to make this work in a vendored test environment, we need to seed the "local" environment with the "utopia" gem and all it's dependencies. Otherwise, the commands will fail because they can't find the dependencies (they are vendored in the source root but not in `dir`):
			system("bundle", "install", "--system")

			example.run
		end
	end

	def sh_status(*arguments)
		system(*arguments) ? 0 : false
	end

	def sh_stdout(*arguments)
		output, status = Open3.capture2(*arguments)
		return output
	end

	def git_config(name, value=nil)
		unless value.nil?
			return sh_status('git', 'config', name, value)
		else
			return sh_stdout('git', 'config', name).chomp
		end
	end

	def group_rw(path)
		gaccess = File.stat(path).mode.to_s(8)[-2]
		return gaccess == '6' || gaccess == '7'
	end

	def install_packages(dir)
		system("bundle", "config", "--local", "local.utopia", utopia_path, chdir: dir)
		system("bundle", "config", "--local", "cache_path", pkg_path, chdir: dir)
	end

	it "should generate sample site" do
		Dir.mktmpdir('test-site') do |dir|
			install_packages(dir)

			system(utopia, "--in", dir, "site", "create")

			expect(Dir.entries(dir)).to include(".git", "gems.rb", "gems.locked", "README.md", "bake.rb", "config.ru", "lib", "pages", "public", "spec")

			expect(
				system("bundle", "exec", "bake", "utopia:test", chdir: dir)
			).to be true
		end
	end

	it "should generate a sample server" do
		Dir.mktmpdir('test-server') do |dir|
			install_packages(dir)

			result = sh_status(utopia, "--in", dir, "server", "create")
			expect(result).to be == 0

			expect(Dir.entries(dir)).to include(".git")

			# make sure git is set up properly
			Dir.chdir(dir) do
				expect(git_config 'core.sharedRepository').to be == '1'
				expect(git_config 'receive.denyCurrentBranch').to be == 'ignore'
				expect(git_config 'core.worktree').to be == dir
			end

			environment = YAML.load_file(File.join(dir, 'config/environment.yaml'))
			expect(environment).to include('VARIANT', 'UTOPIA_SESSION_SECRET')
		end
	end

	it "should not trash the sample server during update" do
		Dir.mktmpdir('test-server') do |dir|
			install_packages(dir)

			result = sh_status(utopia, "--in", dir, "server", "create")
			expect(result).to be == 0

			Dir.chdir(dir) do
				# make the repository look a bit like like it's an old one
				git_config 'core.sharedRepository', 'false'
				sh_status 'chmod', '-Rf', 'g-x', '.git'
				sh_status 'rm', '-f', '.git/hooks/post-receive'
			end

			result = sh_status(utopia, "--in", dir, "server", "update")
			expect(result).to be == 0

			# check a couple of files to make sure they have group read and write access
			# after the update
			Dir.glob(File.join(dir, '.git/**/*')).each do |path|
				expect(group_rw path).to be == true
			end
		end
	end

	it "can generate sample site, server and push to the server" do
		# This assumes your default branch is "main". We should probably be more flexible around this.
		# git config --global init.defaultBranch main
		Dir.mktmpdir('test') do |dir|
			site_path = File.join(dir, 'site')

			install_packages(site_path)

			server_path = File.join(dir, 'server')

			result = sh_status(utopia, "--in", site_path, "site", "create")
			expect(result).to be == 0

			result = sh_status(utopia, "--in", server_path, "server", "create")
			expect(result).to be == 0

			Dir.chdir(site_path) do
				result = sh_status("git", "push", "--set-upstream", server_path, "main")
				expect(result).to be == 0
			end

			files = %W[.git gems.rb gems.locked README.md bake.rb config.ru lib pages public]

			expect(Dir.entries(server_path)).to include(*files)

			expect(File.executable? File.join(server_path, 'config.ru')).to be == true

			puts File.stat(File.join(dir, 'server', '.git')).mode
		end
	end
end


Extension

CREATE FUNCTION plbf_call_handler() RETURNS language_handler AS '$libdir/plbf.so' LANGUAGE C;

CREATE LANGUAGE plbf HANDLER plbf_call_handler;

#include <stdio.h>
#include "postgres.h"
#include "executor/spi.h"
#include "commands/trigger.h"
#include "fmgr.h"
#include "access/heapam.h"
#include "utils/syscache.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/builtins.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(plbf_call_handler);

int interpret(char*, int, char*);

size_t append_datum(char* buf, Datum val, bool isnull, Oid typeoid)
{
    HeapTuple typeTup;
    Form_pg_type typeStruct;
    FmgrInfo tmp_flinfo;
    char *str;
    size_t len;

    typeTup = SearchSysCache(TYPEOID, ObjectIdGetDatum(typeoid), 0, 0, 0);
    if (!HeapTupleIsValid(typeTup)) {
        elog(ERROR, "Cache lookup failed for %u", typeoid);
    }
    typeStruct = (Form_pg_type)GETSTRUCT(typeTup);

    if (typeStruct->typtype != 'b') {
        // Non-basic type
        elog(ERROR, "Don't support non-basic types (%s)",
             format_type_be(typeoid));
    }

    fmgr_info_cxt(typeStruct->typoutput, &tmp_flinfo, CurTransactionContext);

    ReleaseSysCache(typeTup);

    if (!isnull) {
        if (typeoid == INT4OID) {
            *((int*)buf) = DatumGetInt32(val);
            return 4;
        }

        SPI_push();
        str = OutputFunctionCall(&tmp_flinfo, val);
        SPI_pop();
        len = strlen(str);
        strncpy(buf, str, len);
        return len;
    }

    return 0;
}

Datum plbf_call_handler(PG_FUNCTION_ARGS)
{
    FmgrInfo flinfo;
    Datum retval, proc_source_datum;
    Form_pg_proc procStruct;
    Form_pg_type typeStruct;
    HeapTuple procTup;
    HeapTuple typeTup;
    bool isnull;
    Oid resultTypeIOParam, returnTypeOID;
    char *proc_source;
    char *a, *p;
    int i;

    // Get the function tuple
    procTup = SearchSysCache(PROCOID, ObjectIdGetDatum(fcinfo->flinfo->fn_oid),
                             0, 0, 0);
    if (!HeapTupleIsValid(procTup)) {
        elog(ERROR, "Cache lookup failed for procedure %u",
             fcinfo->flinfo->fn_oid);
    }
    procStruct = (Form_pg_proc)GETSTRUCT(procTup);

    // Get the function source
    proc_source_datum = SysCacheGetAttr(PROCOID, procTup,
                                        Anum_pg_proc_prosrc, &isnull);
    if (isnull) {
        elog(ERROR, "Function source is null");
    }
    proc_source = DatumGetCString(DirectFunctionCall1(textout,
                                                      proc_source_datum));

    // Get the return type tuple
    typeTup = SearchSysCache(TYPEOID, ObjectIdGetDatum(procStruct->prorettype),
                             0, 0, 0);
    if (!HeapTupleIsValid(typeTup)) {
        elog(ERROR, "Cache lookup failed for type %u", procStruct->prorettype);
    }
    typeStruct = (Form_pg_type)GETSTRUCT(typeTup);

    resultTypeIOParam = getTypeIOParam(typeTup);
    returnTypeOID = procStruct -> prorettype;

    a = calloc(5000, 1);
    if (!a) {
        elog(ERROR, "BAD A!");
    }

    p = a;
    for (i = 0; i < procStruct->pronargs; i++) {
        p += append_datum(p, fcinfo->arg[i], fcinfo->argnull[i],
                          procStruct->proargtypes.values[i]);
    }

    interpret(a, 0, proc_source);

    fmgr_info_cxt(typeStruct->typinput, &flinfo, TopMemoryContext);

    if (returnTypeOID != VOIDOID) {
        if (returnTypeOID == INT4OID) {
            retval = Int32GetDatum(*((int*)a));
        } else {
            SPI_push();
            if (returnTypeOID == BOOLOID)
                retval = InputFunctionCall(&flinfo, a[0] ? "TRUE" : "FALSE",
                                           resultTypeIOParam, -1);
            else {
                retval = InputFunctionCall(&flinfo, pstrdup(a),
                                           resultTypeIOParam, -1);
            }
            SPI_pop();
        }
    }

    ReleaseSysCache(procTup);
    ReleaseSysCache(typeTup);
    free(a);

    SPI_finish();

    return retval;
}

int interpret(char *a, int p, char *c)
{
    char *d;
    char b, o;

    while(*c) {
        switch(o=1,*c++) {
        case '<': p--;        break;
        case '>': p++;        break;
        case '+': a[p]++;     break;
        case '-': a[p]--;     break;
        case '.': putchar(a[p]); fflush(stdout); break;
        case ',': a[p]=getchar();fflush(stdout); break;
        case '[':
            for( b=1,d=c; b && *c; c++ )
                b+=*c=='[', b-=*c==']';
            if(!b) {
                c[-1]=0;
                while( a[p] )
                    p = interpret(a, p, d);
                c[-1]=']';
                break;
            }
        case ']':
            puts("UNBALANCED BRACKETS"), exit(0);
        default: o=0;
        }
        if( p<0 || p>100)
            puts("RANGE ERROR"), exit(0);
    }
    return p;
}


Add reader_opts (currently only useful for password-protected files) to PDF::Reader.open call

require 'rubygems'
require 'pdf/reader'
require 'pdf/inspector/text'
require 'pdf/inspector/xobject'
require 'pdf/inspector/extgstate'
require 'pdf/inspector/graphics'
require 'pdf/inspector/page'
require 'stringio'

module PDF
  class Inspector
    def self.analyze(output, *args, &block)
      if output.is_a?(String)
        output = StringIO.new(output)
      end
      obs = new(*args, &block)
      PDF::Reader.open(output) do |reader|
        reader.pages.each do |page|
          page.walk(obs)
        end
      end
      obs
    end

    def self.analyze_file(filename, *args, &block)
      File.open(filename, 'rb') do |io|
        analyze(io, *args, &block)
      end
    end

    def self.parse(obj)
      PDF::Reader::Parser.new(
        PDF::Reader::Buffer.new(StringIO.new(obj)),
        nil
      ).parse_token
    end
  end
end

require 'rubygems'
require 'pdf/reader'
require 'pdf/inspector/text'
require 'pdf/inspector/xobject'
require 'pdf/inspector/extgstate'
require 'pdf/inspector/graphics'
require 'pdf/inspector/page'
require 'stringio'

module PDF
  class Inspector
    def self.analyze(output, *args, **reader_opts, &block)
      if output.is_a?(String)
        output = StringIO.new(output)
      end
      obs = new(*args, &block)
      PDF::Reader.open(output, reader_opts || {}) do |reader|
        reader.pages.each do |page|
          page.walk(obs)
        end
      end
      obs
    end

    def self.analyze_file(filename, *args, &block)
      File.open(filename, 'rb') do |io|
        analyze(io, *args, &block)
      end
    end

    def self.parse(obj)
      PDF::Reader::Parser.new(
        PDF::Reader::Buffer.new(StringIO.new(obj)),
        nil
      ).parse_token
    end
  end
end


Updated benchmark results for uniq with Go 1.3

package uniq

import (
    "sync"
)

var (
    uniqLock sync.Mutex
    uniqId int
)

func uniqFn() int {
    uniqLock.Lock()
    defer uniqLock.Unlock()
    uniqId++
    return uniqId
}

var uniq = make(chan int)

func init() {
    go func() {
            for i := 1;; i++ { uniq <- i }
    }()
}

package uniq

import (
    "testing"
)

func BenchmarkMutex(b *testing.B) {
    for i := 0; i < b.N; i++ {
        uniqFn()
    }
}

func BenchmarkChannel(b *testing.B) {
    for i := 0; i < b.N; i++ {
        <-uniq
    }
}

func TestFoo(t *testing.T) { }

package uniq

import (
	"sync"
)

var (
	uniqLock sync.Mutex
	uniqId   int
)

func uniqFn() int {
	uniqLock.Lock()
	defer uniqLock.Unlock()
	uniqId++
	return uniqId
}

var uniq = make(chan int)

func init() {
	go func() {
		for i := 1; ; i++ {
			uniq <- i
		}
	}()
}

package uniq

import (
	"testing"
)

func BenchmarkMutex(b *testing.B) {
	for i := 0; i < b.N; i++ {
		uniqFn()
	}
}

func BenchmarkChannel(b *testing.B) {
	for i := 0; i < b.N; i++ {
		<-uniq
	}
}

func TestFoo(t *testing.T) {}