340 lines
8.0 KiB
JavaScript
340 lines
8.0 KiB
JavaScript
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;
|