summaryrefslogtreecommitdiffstats
path: root/lib/backbone/summary.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/backbone/summary.js')
-rw-r--r--lib/backbone/summary.js339
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;