--- --- "use strict" $ -> $form = $('#search-form') $input = $('#search-input') $similar = $('#similar') $similarList = $('#similar-list') $matchesString = $('#matches') $results = $('#results') $progress = $('#progress') template = $('#template').html() searchIndex = null ajax = new $.Deferred() wordsplit = /[\s,;!?=\+"'\\\/]+/ typeTimer = null generateWordList = -> window.wordlistAll = {} searchIndex.pages.forEach (obj, pageId) -> # Uppercase is a work-around to avoid reserved words # (Thanks, JavaScript!) obj.wordlist.toUpperCase().split(/\s+/).forEach (word) -> return if word == '' wordlistAll[word] ||= [] try wordlistAll[word].push pageId catch e console.log "Problem:", word, pageId return searchIndex.words = wordlistAll loadsearchIndex = -> return if ajax.state() == "resolved" && ajax.state() == "pending" $.get '{{ site.baseurl }}/search.json', (data)-> searchIndex = data do generateWordList unless data.words ajax.resolve data displayResults = (words, matches, similar) -> $pending = $('
') pages = searchIndex.pages text = $input.val().toLowerCase().replace(/ ?-\w+/g, '').replace(/\+/g, '') $('#results-none').hide() if matches.length > 0 fakedate = (new Date).toISOString() matches.sort (a, b) -> # Sort by date dateA = pages[a].date || fakedate dateB = pages[b].date || fakedate # Title match score (weighted heavily) titleA = pages[a].title.toLowerCase().score(text) titleB = pages[b].title.toLowerCase().score(text) # Summary snippet score (weighted a good bit) summA = pages[a].summary.toLowerCase().score(text) summB = pages[b].summary.toLowerCase().score(text) # Date score (very small weight) dateScoreA = Date.parse(dateA.split(' ')[0]) / Date.now() * 0.05 dateScoreB = Date.parse(dateB.split(' ')[0]) / Date.now() * 0.05 # Generate page scores # (and add a 'score' value to the page info) scoreA = pages[a].score = (titleA * 3 + summA + dateScoreA) / 5 / 0.71 scoreB = pages[b].score = (titleB * 3 + summB + dateScoreB) / 5 / 0.71 # Sort based on score return scoreB - scoreA for match in matches page = pages[match] # Clone search result snippet $snippet = $(template) # Add data to the cloned template $snippet.removeClass('hidden') .find('.title').html(page.title).attr('href', page.url).end() .find('.url').html(page.url).attr('href', page.url).end() .find('.summary').html(page.summary).end() if page.date $snippet.find('.date').text(page.datestring).end() else $snippet.find('.date').remove().end() # Add snippet to pending (off-page) search results $pending.append $snippet else if $input.val() != '' $('#results-none').removeClass('hidden').show() # Add search results to the page $results.html($pending.children()) if similar.length > 0 $similar.removeClass('hidden').hide() $similarList.html -> result = '' $.each similar, (k, v)-> rating = "Score: " + Math.round(v.score * 50) / 10 result += '% '.replace(/%/g, v.word).replace(/\$/, rating) return result $similar.show() else $similar.hide() $matchesString.hide() $similarList.html('') $matchesString .find('.number').html(matches.length).end() .find('.plural').toggle(matches.length != 1).end() .removeClass('hidden').toggle(matches.length != 0) $progress.hide() $input.focus() parseForm = -> $progress.removeClass('hidden').show() $.when(ajax).done (data)-> text = $input.val() words = text.toLowerCase().trim().split(wordsplit) similar = [] matchers = {matches: [], antimatches: []} # Search for each word in the text entry for word in words # Check for "-" before a word to exclude that word polarity = if word.match(/^-/) then 'antimatches' else 'matches' # Strip dashes at the beginning and periods at the end of a word word = word.replace(/^-/, '').replace(/\.$/, '') # Search for the existence of words wordMatch = data.words[word.toUpperCase()] if wordMatch if matchers[polarity].length < 1 # First word; propagate the matches matchers[polarity] = wordMatch else if polarity == 'matches' # Do an AND of matching pages matchers[polarity] = matchers[polarity].map (a) -> return a unless wordMatch.indexOf(a) < 0 else # Add unique antimatchers matchers[polarity] = matchers[polarity] .concat wordMatch.filter (item) -> matchers[polarity].indexOf(item) < 0 # Add similar words to a 'similar' keyword array if polarity == 'matches' $.map searchIndex.words, (data, key) -> keyLow = key.toLowerCase() matchscore = keyLow.score(word, 0.5) if (matchscore > 0.7) && !similar.filter((s) -> s.word == keyLow)[0] && word != keyLow similar.push {word: keyLow, score: matchscore} # Sort similar words by similarity score similar.sort (a, b) -> a.score < b.score ? a : b # Power results with a matches-antimatches reaction results = matchers['matches'].filter (match) -> match if matchers['antimatches'].indexOf(match) < 0 # Hand off word matches and similar keywords to the renderer displayResults words, results, similar.slice(0,21) selectForm = -> setTimeout -> $input.focus().select() , 0 updateHash = -> window.location.hash = "#" + $input.val().trim() window.trigger404 = (val) -> if $input.val() == '' keywords= decodeURIComponent(document.location.pathname) .replace val, '' .replace '{{ site.permalink | split: '/' | first }}', '' .replace /.html$/, '' .replace /.md$/, '' .replace /[\/\-\_\+]/g, ' ' .replace /^\s+|\s+$/g, '' $input.val(keywords) do parseForm do loadsearchIndex $form .on 'keydown', (e)-> switch e.key when "Tab", "Down", "ArrowDown" $('#similar a:first,#results a').first().focus() do e.preventDefault when "Left", "ArrowLeft", "Right", "ArrowRight", "Home", "End", "Esc", "Escape", "Enter" # NOOP else unless e.ctrlKey || e.altKey clearTimeout typeTimer typeTimer = setTimeout updateHash, 500 .on 'submit', (e)-> do updateHash do e.preventDefault $similar.on 'keydown', 'a', (e)-> switch e.key when "Right", "ArrowRight" $(@).next().focus() when "Left", "ArrowLeft" $(@).prev().focus() when "Up", "ArrowUp" selectForm() when "Down", "ArrowDown" $('#results a:first').focus() when " " $(@).click() when "Enter" selectForm() $results .on 'keydown', 'a', (e) -> $article = $(@).parentsUntil('#results') switch e.key when "Down", "ArrowDown" $article.next().find('a:first').focus() when "Up", "ArrowUp" firstChild = $article.filter(':first-child').length != 1 if firstChild selectForm() else $article.prev().find('a:first').focus() .on 'focus', 'a', (e) -> $(@).parentsUntil('#results').addClass('highlight') .on 'blur', 'a', (e) -> $(@).parentsUntil('#results').removeClass('highlight') $(document) .on 'keydown', (e) -> switch e.key when "Esc", "Escape" $input.val('') do updateHash $.when(ajax).done (data)-> do parseForm $(window).on 'hashchange', (e)-> val = @location.hash.substr(1) $input.val(decodeURIComponent(val)) do parseForm # Run a search if the URL is like "/search/#foo" $(window).trigger 'hashchange' if window.location.hash selectForm()