diff options
Diffstat (limited to 'lib/backbone/summary.js')
-rw-r--r-- | lib/backbone/summary.js | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js new file mode 100644 index 0000000..4ae3453 --- /dev/null +++ b/lib/backbone/summary.js @@ -0,0 +1,339 @@ +var _ = require('lodash'); +var util = require('util'); +var url = require('url'); + +var location = require('../utils/location'); +var error = require('../utils/error'); +var BackboneFile = require('./file'); + + +/* + An article represent an entry in the Summary. + It's defined by a title, a reference, and children articles, + the reference (ref) can be a filename + anchor or an external file (optional) +*/ +function TOCArticle(def, parent) { + // Title + this.title = def.title; + + // Parent TOCPart or TOCArticle + this.parent = parent; + + // As string indicating the overall position + // ex: '1.0.0' + this.level; + this._next; + this._prev; + + // When README has been automatically added + this.isAutoIntro = def.isAutoIntro; + this.isIntroduction = def.isIntroduction; + + this.validate(); + + // Path can be a relative path or an url, or nothing + this.ref = def.path; + if (this.ref) { + var parts = url.parse(this.ref); + + if (!this.isExternal()) { + this.path = parts.pathname; + this.anchor = parts.hash; + } + } + + this.articles = _.map(def.articles || [], function(article) { + if (article instanceof TOCArticle) return article; + return new TOCArticle(article, this); + }, this); +} + +// Validate the article +TOCArticle.prototype.validate = function() { + if (!this.title) { + throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title')); + } +}; + +// Iterate over all articles in this articles +TOCArticle.prototype.walk = function(iter, base) { + base = base || this.level; + + _.each(this.articles, function(article, i) { + var level = levelId(base, i); + + if (iter(article, level) === false) { + return false; + } + article.walk(iter, level); + }); +}; + +// Return templating context for an article +TOCArticle.prototype.getContext = function() { + return { + level: this.level, + title: this.title, + depth: this.depth(), + path: this.isExternal()? undefined : this.path, + anchor: this.isExternal()? undefined : this.anchor, + url: this.isExternal()? this.ref : undefined + }; +}; + +// Return true if is pointing to a file +TOCArticle.prototype.hasLocation = function() { + return Boolean(this.path); +}; + +// Return true if is pointing to an external location +TOCArticle.prototype.isExternal = function() { + return location.isExternal(this.ref); +}; + +// Return true if this article is the introduction +TOCArticle.prototype.isIntro = function() { + return Boolean(this.isIntroduction); +}; + +// Return true if has children +TOCArticle.prototype.hasChildren = function() { + return this.articles.length > 0; +}; + +// Return true if has an article as parent +TOCArticle.prototype.hasParent = function() { + return !(this.parent instanceof TOCPart); +}; + +// Return depth of this article +TOCArticle.prototype.depth = function() { + return this.level.split('.').length; +}; + +// Return next article in the TOC +TOCArticle.prototype.next = function() { + return this._next; +}; + +// Return previous article in the TOC +TOCArticle.prototype.prev = function() { + return this._prev; +}; + +// Map over all articles +TOCArticle.prototype.map = function(iter) { + return _.map(this.articles, iter); +}; + + +/* + A part of a ToC is a composed of a tree of articles and an optiona title +*/ +function TOCPart(part, parent) { + if (!(this instanceof TOCPart)) return new TOCPart(part, parent); + + TOCArticle.apply(this, arguments); +} +util.inherits(TOCPart, TOCArticle); + +// Validate the part +TOCPart.prototype.validate = function() { }; + +// Return a sibling (next or prev) of this part +TOCPart.prototype.sibling = function(direction) { + var parts = this.parent.parts; + var pos = _.findIndex(parts, this); + + if (parts[pos + direction]) { + return parts[pos + direction]; + } + + return null; +}; + +// Iterate over all entries of the part +TOCPart.prototype.walk = function(iter, base) { + var articles = this.articles; + + if (articles.length == 0) return; + + // Has introduction? + if (articles[0].isIntro()) { + if (iter(articles[0], '0') === false) { + return; + } + + articles = articles.slice(1); + } + + + _.each(articles, function(article, i) { + var level = levelId(base, i); + + if (iter(article, level) === false) { + return false; + } + + article.walk(iter, level); + }); +}; + +// Return templating context for a part +TOCPart.prototype.getContext = function(onArticle) { + onArticle = onArticle || function(article) { + return article.getContext(); + }; + + return { + title: this.title, + articles: this.map(onArticle) + }; +}; + +/* +A summary is composed of a list of parts, each composed wit a tree of articles. +*/ +function Summary() { + BackboneFile.apply(this, arguments); + + this.parts = []; + this._length = 0; +} +util.inherits(Summary, BackboneFile); + +Summary.prototype.type = 'summary'; + +// Prepare summary when non existant +Summary.prototype.parseNotFound = function() { + this.update([]); +}; + +// Parse the summary content +Summary.prototype.parse = function(content) { + var that = this; + + return this.parser.summary(content) + + .then(function(summary) { + that.update(summary.parts); + }); +}; + +// Return templating context for the summary +Summary.prototype.getContext = function() { + function onArticle(article) { + var result = article.getContext(); + if (article.hasChildren()) { + result.articles = article.map(onArticle); + } + + return result; + } + + return { + summary: { + parts: _.map(this.parts, function(part) { + return part.getContext(onArticle); + }) + } + }; +}; + +// Iterate over all entries of the summary +// iter is called with an TOCArticle +Summary.prototype.walk = function(iter) { + var hasMultipleParts = this.parts.length > 1; + + _.each(this.parts, function(part, i) { + part.walk(iter, hasMultipleParts? levelId('', i) : null); + }); +}; + +// Find a specific article using a filter +Summary.prototype.find = function(filter) { + var result; + + this.walk(function(article) { + if (filter(article)) { + result = article; + return false; + } + }); + + return result; +}; + +// Return the first TOCArticle for a specific page (or path) +Summary.prototype.getArticle = function(page) { + if (!_.isString(page)) page = page.path; + + return this.find(function(article) { + return article.path == page; + }); +}; + +// Return the first TOCArticle for a specific level +Summary.prototype.getArticleByLevel = function(lvl) { + return this.find(function(article) { + return article.level == lvl; + }); +}; + +// Return the count of articles in the summary +Summary.prototype.count = function() { + return this._length; +}; + +// Prepare the summary +Summary.prototype.update = function(parts) { + var that = this; + + + that.parts = _.map(parts, function(part) { + return new TOCPart(part, that); + }); + + // Create first part if none + if (that.parts.length == 0) { + that.parts.push(new TOCPart({}, that)); + } + + // Add README as first entry + var firstArticle = that.parts[0].articles[0]; + if (!firstArticle || firstArticle.path != that.book.readme.path) { + that.parts[0].articles.unshift(new TOCArticle({ + title: 'Introduction', + path: that.book.readme.path, + isAutoIntro: true + }, that.parts[0])); + } + that.parts[0].articles[0].isIntroduction = true; + + + // Update the count and indexing of "level" + var prev = undefined; + + that._length = 0; + that.walk(function(article, level) { + // Index level + article.level = level; + + // Chain articles + article._prev = prev; + if (prev) prev._next = article; + + prev = article; + + that._length += 1; + }); +}; + + +// Return a level string from a base level and an index +function levelId(base, i) { + i = i + 1; + return (base? [base || '', i] : [i]).join('.'); +} + +module.exports = Summary; |