Skip to content

Commit

Permalink
[major] constructor handbook and add searching with lunr
Browse files Browse the repository at this point in the history
  • Loading branch information
Swaagie committed Mar 13, 2013
1 parent 418767d commit fd5cf7d
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 31 deletions.
119 changes: 90 additions & 29 deletions index.js
Expand Up @@ -3,6 +3,11 @@
var path = require('path'),
fs = require('fs'),
natural = require('natural'),
lunr = require('lunr'),
idx = lunr(function setupLunr() {
this.field('title', { boost: 10 });
this.field('body');
}),
tokenizer = new natural.WordTokenizer(),
loc = path.resolve(__dirname, 'content'),
scraper = {
Expand All @@ -11,20 +16,22 @@ var path = require('path'),
};

//
// ### function scrape()
// ### @private function scrape()
// #### @content {String} document content
// #### @key {String} scraper key
// Scrapes the [key] from the content by Regular Epression
//
function scrape(content, key) {
if (!content) return '';

var match = content.replace(/\n/g, ' ').match(scraper[key]);

// Only return scraped content if there is a meta:[key].
return match ? match[1].trim() : '';
}

//
// ### function normalize()
// ### @private function normalize()
// #### @file {String} file name
// Normalize the file name to resolve to a Markdown or index file.
//
Expand All @@ -35,21 +42,21 @@ function normalize(file) {
}

//
// ### function fileContent()
// ### @private function fileContent()
// #### @content {String} Document content
// Sugar content with additional properties from scraped content.
//
function fileContent(content) {
return {
content: content,
content: content || '',
description: scrape(content, 'description'),
title: scrape(content, 'title'),
tags: tags(content, 10)
};
}

//
// ### function frequency()
// ### @private function frequency()
// #### @content {String} Document content
// Return list of words scored by Term Frequency-Inverse Document Frequency.
//
Expand Down Expand Up @@ -79,12 +86,14 @@ function frequency(content) {
}

//
// ### function tags()
// ### @private function tags()
// #### @content {String} Document content
// #### @n {Number} number of tags
// Return n highest scoring tags as determined by term frequency.
//
function tags(content, n) {
if (!content) return [];

return frequency(content).sort(function sortByScore(a, b) {
return b.score - a.score;
}).slice(0, n).map(function extractWords(tag) {
Expand All @@ -93,7 +102,26 @@ function tags(content, n) {
}

//
// ### function walk()
// ### @private function read()
// #### @file {String} Filename
// #### @callback {Function} Callback for file contents
// Returns file content by normalized path, if a callback is provided, content
// is returned asynchronously.
//
function read(file, callback) {
if (!callback) {
return fileContent(fs.readFileSync(path.resolve(loc, normalize(file)), 'utf8'));
}

file = path.resolve(loc, normalize(file));

fs.readFile(file, 'utf8', function read(error, content) {
callback.apply(this, [error, fileContent(content)]);
});
}

//
// ### @private function walk()
// #### @dir {String} Directory path to crawl
// #### @result {Object} Append content to current results
// #### @callback {Function} Callback for sub directory
Expand Down Expand Up @@ -136,7 +164,7 @@ function walk(dir, callback, result, sub) {
if (ref === 'index') name.pop();

// Get the tile of the file.
get(file, function getFile(err, file) {
read(file, function getFile(err, file) {
result[current][ref] = {
href: sub ? name.join('/') : '',
title: file.title,
Expand All @@ -152,7 +180,7 @@ function walk(dir, callback, result, sub) {
}

//
// ### function walkSync()
// ### @private function walkSync()
// #### @dir {String} Directory path to crawl
// #### @result {Object} Append content to current results
// #### @sub {Boolean} Is directory subdirectory of dir
Expand Down Expand Up @@ -189,7 +217,7 @@ function walkSync(dir, result, sub) {
// Append file information to current container.
result[current][ref] = {
href: sub ? name.join('/') : '',
title: get(file).title,
title: read(file).title,
path: dir
};
}
Expand All @@ -198,38 +226,71 @@ function walkSync(dir, result, sub) {
return result;
}

//
// ### @private function prepareSearch()
// #### @toc {Object} Representation of table of contents
// Prepare search index to query against, this method may not be exposed
// as it is synchronously only.
//
function prepareSearch(toc) {
var document;

Object.keys(toc).forEach(function loopSections(section) {
Object.keys(toc[section]).forEach(function loopPages(page) {
page = (section !== 'index' ? section + '/' : '') + page;
document = read(page);

idx.add({
id: page,
title: document.title,
body: document.content
});
});
});
}

//
// ### function Handbook()
// Constructor for easy access to Handbook content.
//
function Handbook() {
// On constructuing handbook synchronously prepare the search index. Listening
// to a search index ready event is not required thus easing the flow.
prepareSearch(walkSync(loc));
}

//
// ### function get()
// #### @file {String} Filename
// #### @callback {Function} Callback for file contents
// Returns file content by normalized path, if a callback is provided, content
// is returned asynchronously.
// Proxy method to private async method read. This method can be called
// synchronously by omitting the callback.
//
function get(file, callback) {
if (!callback) {
return fileContent(fs.readFileSync(path.resolve(loc, normalize(file)), 'utf8'));
}

file = path.resolve(loc, normalize(file));

fs.readFile(file, 'utf8', function read(error, content) {
callback.apply(this, [error, fileContent(content)]);
});
}
Handbook.prototype.get = function get(file, callback) {
read.apply(this, arguments);
};

//
// ### function catalog()
// #### @callback {Function} Callback for catalog/TOC
// Returns a catalog by parsing the content directory, if a callback is provided
// content is returned asynchronously.
function catalog(callback) {
//
Handbook.prototype.catalog = function catalog(callback) {
if (!callback) return walkSync(loc);

walk(loc, callback);
}
};

// Expose public functions.
module.exports = {
get: get,
catalog: catalog
//
// ### function search()
// #### @query {String} Query to search against
// Proxy to the search method of Lunr, returns a list of documents, due to Lunr
// this must be called on scope of Lunr.
//
Handbook.prototype.search = function (query) {
return idx.search.call(idx, query);
};

// Expose public functions.
module.exports = Handbook;
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -2,7 +2,7 @@
"author": "Nodejitsu <support@nodejitsu.com> (http://nodejitsu.com)",
"name": "handbook",
"description": "A gentle introduction to the art of nodejitsu",
"version": "0.3.5",
"version": "0.4.0",
"subdomain": "handbook",
"homepage": "http://www.nodejitsu.com/docs",
"repository": {
Expand All @@ -13,7 +13,8 @@
"start": "bin/server"
},
"dependencies": {
"natural": "0.1.19"
"natural": "0.1.19",
"lunr": "0.2.x"
},
"engines": {
"node": "0.8.x"
Expand Down

0 comments on commit fd5cf7d

Please sign in to comment.