Created
November 10, 2008 22:11
-
-
Save graynorton/23656 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A dependency, from Douglas Crockford's "JavaScript: The Good Parts" | |
if (typeof Object.create !== 'function') { | |
Object.create = function (o) { | |
var F = function () {}; | |
F.prototype = o; | |
return new F(); | |
}; | |
} | |
// freebaseNounType | |
/////////////////////////////////////////////////// | |
// Builds Ubiquity nouns based on Freebase types | |
function freebaseNounType( name, typeFilter ) { | |
var that, onPause; | |
onPause = new Waiter( 400 ); | |
that = { | |
_name: name, | |
suggest: function suggest( text, html, callback ) { | |
onPause.please( function() { freebaseNounType.manager.getSuggestions( text, callback, typeFilter ) } ); | |
return []; | |
} | |
} | |
return that; | |
} | |
freebaseNounType.manager = freebaseNounTypeManager(); | |
// freebaseNounTypeManager | |
////////////////////////////////////////////////////////////////// | |
// Coordinates simultaneous requests from multiple Freebase nouns, | |
// allowing them to be relevance-sorted together and returned in | |
// predictable order | |
function freebaseNounTypeManager() { | |
var that, maxSuggestions, openRequests; | |
that = {}; | |
maxSuggestions = 5; | |
openRequests = {}; | |
function request( text ) { | |
var that, subRequests, subRequestsClosed, afterLastSubRequest, suggestions, closed, suggested; | |
that = {}; | |
subRequests = 0; | |
subRequestsClosed = 0; | |
afterLastSubRequest = new Waiter( 50 ); | |
suggestions = []; | |
closed = false; | |
suggested = false; | |
function queueSuggestion( result, callback ) { | |
var suggestion = function suggestion() { callback( CmdUtils.makeSugg( result.name, result.name, result ) ); }; | |
suggestion.score = result[ "relevance:score" ]; | |
suggestions.push( suggestion ); | |
} | |
function makeSuggestions() { | |
suggestions = suggestions.sort( function( a, b ) { | |
var scoreA = parseFloat( a.score ); | |
var scoreB = parseFloat( b.score ); | |
if ( scoreA > scoreB ) { | |
return -1; | |
} | |
if ( scoreA < scoreB ) { | |
return 1; | |
} | |
return 0; | |
} ); | |
for ( var i = 0; i < suggestions.length && i < maxSuggestions; i++ ) { | |
suggestions[ i ](); | |
} | |
suggested = true; | |
} | |
function close() { | |
delete openRequests[ text ]; | |
closed = true; | |
if ( !suggested && openSubRequests() === 0 ) { | |
makeSuggestions(); | |
} | |
} | |
function openSubRequest() { | |
subRequests++; | |
afterLastSubRequest.please( close ); | |
} | |
function closeSubRequest() { | |
subRequestsClosed ++; | |
if ( closed && openSubRequests() === 0 ) { | |
makeSuggestions(); | |
} | |
} | |
function openSubRequests() { | |
return subRequests - subRequestsClosed; | |
} | |
that.queueSuggestion = queueSuggestion; | |
that.openSubRequest = openSubRequest; | |
that.closeSubRequest = closeSubRequest; | |
openRequests[ text ] = that; | |
return that; | |
} | |
function getSuggestions( text, callback, typeFilter ) { | |
var req = openRequests[ text ] || request( text ); | |
req.openSubRequest(); | |
var params = { | |
prefix: text, | |
limit: maxSuggestions, | |
success: function success( freebaseResponse ) { | |
var results = freebaseResponse.result; | |
for ( var idx = 0; idx < results.length; idx++ ) { | |
var result = results[idx]; | |
req.queueSuggestion( result, callback ); | |
} | |
req.closeSubRequest(); | |
} | |
}; | |
if ( typeFilter && typeFilter.length ) { | |
params.typeFilter = typeFilter; | |
} | |
Freebase.search( params ); | |
} | |
that.getSuggestions = getSuggestions; | |
return that; | |
} | |
// Freebase | |
/////////////////////////////////////////////////////////////////// | |
// Reusable Freebase functionality, wrapping Freebase API calls | |
var Freebase = { | |
_decodeWin1252: function _decodeWin1252( str ) { | |
return str.replace( /\$([0-9A-F]{4})/g, | |
function( whole, val ) { | |
return String.fromCharCode( parseInt( val, 16 ) ); | |
} ); | |
}, | |
wrapQuery: function wrapQuery( query, queryLabel ) { | |
if ( !queryLabel ) { | |
queryLabel = "response"; | |
} | |
var inner = { "query" : query }; | |
var outer = {}; | |
outer[ queryLabel ] = inner; | |
return outer; | |
}, | |
// id*, maxwidth, maxheight, mode, onfail | |
imageThumbUrl: function imageThumbUrl( params ) { | |
if ( params.id === undefined ) { return ""; } | |
var url = "http://www.freebase.com/api/trans/image_thumb/" + params.id + "?"; | |
for ( var param in params ) { | |
if ( param === "id" ) { continue; } | |
url += param + "=" + encodeURIComponent( params[ param ] ) + "&"; | |
} | |
return url; | |
}, | |
// id*, maxlength, break_paragraphs | |
blurb: new UbiquityAjaxFn( { | |
name: "Freebase blurb", | |
url: "http://www.freebase.com/api/trans/blurb", | |
args: { maxlength: 0, break_paragraphs: 0 }, | |
dataType: "text", | |
beforeBuild: function() { | |
this.url += this.id; | |
delete this.id; | |
} | |
} ), | |
// (query|prefix)*, type, limit | |
search: new UbiquityAjaxFn( { | |
name: "Freebase search", | |
url: "http://www.freebase.com/api/service/search", | |
args: { query: 1, prefix: 1, typeFilter: 0, limit: 0 }, | |
beforeBuild: function beforeBuild() { | |
if ( this.data.typeFilter !== undefined ) { | |
this.data.type = this.data.typeFilter; | |
delete this.data.typeFilter; | |
} | |
} | |
} ), | |
// query* | |
mqlRead: new UbiquityAjaxFn( { | |
name: "Freebase mqlRead", | |
type: "POST", | |
url: "http://www.freebase.com/api/service/mqlread", | |
beforeBuild: function() { | |
this.data = "queries=" + Utils.encodeJson( Freebase.wrapQuery( this.query ) ); | |
delete this.query; | |
}, | |
dataFilter: function( data, type ) { | |
return Freebase._decodeWin1252( data ); | |
} | |
} ) | |
} | |
// UbiquityAjaxFn | |
////////////////////////////////////////////////////////////////////////////// | |
// Builds Ajax functions for use in Ubiquity nouns and previews: sets some | |
// defaults, handles caching and calls CmdUtils.previewAjax when applicable | |
function UbiquityAjaxFn( defParams ) { | |
var defaults = { | |
name: "Ajax", | |
args: {}, | |
error: function error( XMLHttpRequest, textStatus, errorThrown ) { | |
CmdUtils.log( p.name + " error: " + textStatus ); | |
}, | |
success: function success( data, textStatus ) { | |
var responseDisplay = ( typeof data === "Object" ) ? Utils.encodeJson( data ) : data; | |
CmdUtils.log( p.name + " response: " + responseDisplay ); | |
}, | |
dataType: "json", | |
cache: true | |
} | |
function _processArgs() { | |
if ( this.data === undefined ) { | |
this.data = {}; | |
} | |
for ( var arg in this.args ) { | |
if ( this[ arg ] !== undefined ) { | |
this.data[ arg ] = this[ arg ]; | |
delete this[ arg ]; | |
} | |
} | |
} | |
var p = Object.create( defaults ); | |
for ( var key in defParams ) { | |
p[ key ] = defParams[ key ]; | |
} | |
var cache = ( p.cache ) ? new Cache() : null; | |
var fn = function( runParams ) { | |
var r = Object.create( p ); | |
var cacheSignature = {}; | |
for ( var key in runParams ) { | |
r[ key ] = runParams[ key ]; | |
// Use runtime params as keys for the cache, but omit functions and DOM elements | |
if ( typeof runParams[ key ] !== "function" && runParams[ key ].nodeType === undefined ) { | |
cacheSignature[ key ] = runParams[ key ]; | |
} | |
} | |
_processArgs.call( r ); | |
if ( r.beforeBuild !== undefined ) { | |
r.beforeBuild.call( r ); | |
} | |
if ( cache && r.cache ) { | |
var cachedResponse = cache.read( cacheSignature ); | |
if ( cachedResponse ) { | |
return r.success( cachedResponse ); | |
} | |
var origSuccess = r.success; | |
r.success = function success( data, textStatus ) { | |
cache.write( cacheSignature, data ); | |
origSuccess( data, textStatus ); | |
} | |
} | |
if ( r.container === undefined ) { | |
jQuery.ajax( r ); | |
} | |
else { | |
CmdUtils.previewAjax( r.container, r ); | |
} | |
} | |
return fn; | |
} | |
// Cache | |
///////////////////////////////////////// | |
// Simple cache, used by UbiquityAjaxFn | |
function Cache() { | |
var _cache = {}; | |
function _key( request ) { | |
if ( typeof request === "object" ) { | |
var key = Utils.encodeJson( request ); | |
return key; | |
} | |
return query; | |
} | |
this.write = function write( request, response ) { | |
_cache[ _key( request ) ] = response; | |
} | |
this.read = function read( request ) { | |
var response = _cache[ _key( request ) ]; | |
return response; | |
} | |
} | |
// Waiter | |
///////////////////////////////////////////////////////////// | |
// Used by Freebase nouns to wait for a pause in typing so | |
// that we don't pepper Freebase with unnecessary requests, | |
// and to wait for suggestions to stop coming back before | |
// returning them from the queue | |
function Waiter( delay ) { | |
if ( !delay ) { delay = 300; } | |
var timer = null; | |
this.please = function please( fn ) { | |
if (timer) { | |
Utils.clearTimeout( timer ); | |
} | |
timer = Utils.setTimeout ( function() { | |
timer = null; | |
fn(); | |
}, delay ); | |
} | |
} | |
////////////////////// | |
// Currently Unused // | |
////////////////////// | |
/* | |
// Was doing my own Freebase noun search before discovering Freebase's search service. | |
// With a bit more work, might still be useful, especially for generating | |
// nouns that need to differentiate based on something more than just name | |
// and type. | |
_get: function _get( callback, typeFilter, customParams ) { | |
var params = { | |
name: null, | |
id: null, | |
type: [ { | |
attribution : "/user/metaweb", | |
id: null, | |
name: null | |
} ], | |
key: [ { | |
// Was using "/wikipedia/en_id", but that required an extra Ajax request | |
// to get the page name, so trying "/wikipedia/en"[0] on the (unverified) | |
// assumption that the first Wikipedia page in each Freebase | |
// record is the canonical page | |
namespace: "/wikipedia/en", | |
value: null, | |
limit: 1 | |
} ], | |
limit: this._maxSuggestions | |
}; | |
if ( typeFilter !== undefined ) { | |
for ( var idx = 0; idx++; idx < typeFilter.length ) { | |
params[ "f" + idx + ":type" ] = typeFilter[ idx ]; | |
} | |
} | |
for ( var key in customParams ) { | |
params[ key ] = customParams[ key ]; | |
} | |
var query = [ params ]; | |
Freebase.mqlRead( { | |
query: query, | |
success: callback | |
} ); | |
}, | |
getTopExactMatch: function getTopExactMatch( text, callback, typeFilter ) { | |
var params = { "name~=": "^" + text.replace(/ /g, "\\ ") + "$", limit: 1 }; | |
this._get( callback, typeFilter, params ); | |
}, | |
getExactMatches: function getExactMatches( text, callback, typeFilter ) { | |
var params = { name: text }; | |
this._get( callback, typeFilter, params ); | |
}, | |
getCompletions: function getCompletions( text, callback, typeFilter ) { | |
var params = { "name~=": "^" + text + "*" }; | |
this._get( callback, typeFilter, params ); | |
}, | |
getSimilar: function getSimilar( text, callback, typeFilter ) { | |
var params = { "name~=": "*" + text + "*" }; | |
this._get( callback, typeFilter, params ); | |
}, | |
getSuggestions: function getSuggestions( text, callback, typeFilter ) { | |
var that = this; | |
var methods = [ this.getTopExactMatch, this.getCompletions, this.getSimilar ]; | |
var currentMethod = 0; | |
var suggested = {}, numSuggested = 0; | |
tryMethod(); | |
function tryMethod() { | |
methods[ currentMethod ].call( that, text, processResults, typeFilter); | |
function processResults( freebaseResponse ) { | |
var results = freebaseResponse.response.result; | |
for ( var idx in results ) { | |
var result = results[ idx ]; | |
if ( suggested[ result.id ] === undefined ) { | |
callback( CmdUtils.makeSugg( result.name, result.name, result ) ); | |
suggested[ result.id ] = true; | |
if ( ++numSuggested === that._maxSuggestions) return; | |
} | |
} | |
if ( numSuggested < that._maxSuggestions && ++currentMethod < methods.length ) tryMethod(); | |
} | |
} | |
} | |
// Was getting description and image from Wikipedia OpenSearch before discovering Freebase search | |
function extractDescAndImage( results ) { | |
const maxDim = 150; | |
if (!results.Section.Item) { | |
About.meta.content.description = ""; | |
} | |
else { | |
var item = ( results.Section.Item.Description ) ? results.Section.Item : results.Section.Item[0]; | |
About.meta.content.description = item.Description; | |
if ( item.Image ) { | |
var aspectRatio = item.Image.width / item.Image.height; | |
var targetWidth, targetHeight; | |
if ( aspectRatio > 1 ) { | |
targetWidth = maxDim; | |
targetHeight = Math.round( targetWidth / aspectRatio ); | |
} | |
else { | |
targetHeight = maxDim; | |
targetWidth = Math.round( targetHeight * aspectRatio ); | |
} | |
item.Image.source = item.Image.source.replace( /[0-9]+px/, targetWidth + "px" ); | |
About.meta.content.image = { | |
src: item.Image.source, | |
height: targetHeight, | |
width: targetWidth | |
} | |
} | |
} | |
About.meta.content.status = null; | |
render(); | |
} | |
// Shell for some reusable Wikipedia functionality, just OpenSearch so far. | |
var Wikipedia = { | |
_openSearchCache: new Cache(), | |
openSearch: new UbiquityAjaxFn( { | |
name: "Wikipedia OpenSearch", | |
url: "http://en.wikipedia.org/w/api.php", | |
args: { search: 1 }, | |
dataType: "xml", | |
data: { | |
action: "opensearch", | |
format: "xml" // Asking for XML because it contains info not included in the JSON version (image, abstract, etc.) | |
}, | |
beforeBuild: function beforeBuild() { | |
var origSuccess = this.success; | |
this.success = function success( data, textStatus ) { | |
var json = jQuery.xml2json( data ); | |
origSuccess( json, textStatus ); | |
} | |
} | |
} ) | |
} | |
// Dependency for Wikipedia OpenSearch implementation above | |
jQuery.get( "http://www.fyneworks.com/jquery/xml-to-json/jquery.xml2json.js", | |
null, | |
function( script ) { | |
eval( script ); // using eval because jQuery.getScript doesn't seem to work in Ubiquity Command sandbox | |
}, | |
"text" ); | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment