summaryrefslogtreecommitdiffstats
path: root/lib/backbone
diff options
context:
space:
mode:
Diffstat (limited to 'lib/backbone')
-rw-r--r--lib/backbone/file.js69
-rw-r--r--lib/backbone/glossary.js99
-rw-r--r--lib/backbone/index.js8
-rw-r--r--lib/backbone/langs.js81
-rw-r--r--lib/backbone/readme.js26
-rw-r--r--lib/backbone/summary.js339
6 files changed, 622 insertions, 0 deletions
diff --git a/lib/backbone/file.js b/lib/backbone/file.js
new file mode 100644
index 0000000..209e261
--- /dev/null
+++ b/lib/backbone/file.js
@@ -0,0 +1,69 @@
+var _ = require('lodash');
+
+function BackboneFile(book) {
+ if (!(this instanceof BackboneFile)) return new BackboneFile(book);
+
+ this.book = book;
+ this.log = this.book.log;
+
+ // Filename in the book
+ this.path = '';
+ this.parser;
+
+ _.bindAll(this);
+}
+
+// Type of the backbone file
+BackboneFile.prototype.type = '';
+
+// Parse a backbone file
+BackboneFile.prototype.parse = function() {
+ // To be implemented by each child
+};
+
+// Handle case where file doesn't exists
+BackboneFile.prototype.parseNotFound = function() {
+
+};
+
+// Return true if backbone file exists
+BackboneFile.prototype.exists = function() {
+ return Boolean(this.path);
+};
+
+// Locate a backbone file, could be .md, .asciidoc, etc
+BackboneFile.prototype.locate = function() {
+ var that = this;
+ var filename = this.book.config.getStructure(this.type, true);
+ this.log.debug.ln('locating', this.type, ':', filename);
+
+ return this.book.findParsableFile(filename)
+ .then(function(result) {
+ if (!result) return;
+
+ that.path = result.path;
+ that.parser = result.parser;
+ });
+};
+
+// Read and parse the file
+BackboneFile.prototype.load = function() {
+ var that = this;
+ this.log.debug.ln('loading', this.type, ':', that.path);
+
+ return this.locate()
+ .then(function() {
+ if (!that.path) return that.parseNotFound();
+
+ that.log.debug.ln(that.type, 'located at', that.path);
+
+ return that.book.readFile(that.path)
+
+ // Parse it
+ .then(function(content) {
+ return that.parse(content);
+ });
+ });
+};
+
+module.exports = BackboneFile;
diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js
new file mode 100644
index 0000000..cc0fdce
--- /dev/null
+++ b/lib/backbone/glossary.js
@@ -0,0 +1,99 @@
+var _ = require('lodash');
+var util = require('util');
+var BackboneFile = require('./file');
+
+// Normalize a glossary entry name into a unique id
+function nameToId(name) {
+ return name.toLowerCase()
+ .replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
+ .replace(/ /g, '_')
+ .trim();
+}
+
+
+/*
+A glossary entry is represented by a name and a short description
+An unique id for the entry is generated using its name
+*/
+function GlossaryEntry(name, description) {
+ if (!(this instanceof GlossaryEntry)) return new GlossaryEntry(name, description);
+
+ this.name = name;
+ this.description = description;
+
+ Object.defineProperty(this, 'id', {
+ get: _.bind(this.getId, this)
+ });
+}
+
+// Normalizes a glossary entry's name to create an ID
+GlossaryEntry.prototype.getId = function() {
+ return nameToId(this.name);
+};
+
+
+/*
+A glossary is a list of entries stored in a GLOSSARY.md file
+*/
+function Glossary() {
+ BackboneFile.apply(this, arguments);
+
+ this.entries = [];
+}
+util.inherits(Glossary, BackboneFile);
+
+Glossary.prototype.type = 'glossary';
+
+// Get templating context
+Glossary.prototype.getContext = function() {
+ if (!this.path) return {};
+
+ return {
+ glossary: {
+ path: this.path
+ }
+ };
+};
+
+// Parse the readme content
+Glossary.prototype.parse = function(content) {
+ var that = this;
+
+ return this.parser.glossary(content)
+ .then(function(entries) {
+ that.entries = _.map(entries, function(entry) {
+ return new GlossaryEntry(entry.name, entry.description);
+ });
+ });
+};
+
+// Return an entry by its id
+Glossary.prototype.get = function(id) {
+ return _.find(this.entries, {
+ id: id
+ });
+};
+
+// Find an entry by its name
+Glossary.prototype.find = function(name) {
+ return this.get(nameToId(name));
+};
+
+// Return false if glossary has entries (and exists)
+Glossary.prototype.isEmpty = function(id) {
+ return _.size(this.entries) === 0;
+};
+
+// Convert the glossary to a list of annotations
+Glossary.prototype.annotations = function() {
+ return _.map(this.entries, function(entry) {
+ return {
+ id: entry.id,
+ name: entry.name,
+ description: entry.description,
+ href: '/' + this.path + '#' + entry.id
+ };
+ }, this);
+};
+
+module.exports = Glossary;
diff --git a/lib/backbone/index.js b/lib/backbone/index.js
new file mode 100644
index 0000000..4c3c3f3
--- /dev/null
+++ b/lib/backbone/index.js
@@ -0,0 +1,8 @@
+
+module.exports = {
+ Readme: require('./readme'),
+ Summary: require('./summary'),
+ Glossary: require('./glossary'),
+ Langs: require('./langs')
+};
+
diff --git a/lib/backbone/langs.js b/lib/backbone/langs.js
new file mode 100644
index 0000000..e339fa9
--- /dev/null
+++ b/lib/backbone/langs.js
@@ -0,0 +1,81 @@
+var _ = require('lodash');
+var path = require('path');
+var util = require('util');
+var BackboneFile = require('./file');
+
+function Language(title, folder) {
+ var that = this;
+
+ this.title = title;
+ this.folder = folder;
+
+ Object.defineProperty(this, 'id', {
+ get: function() {
+ return path.basename(that.folder);
+ }
+ });
+}
+
+/*
+A Langs is a list of languages stored in a LANGS.md file
+*/
+function Langs() {
+ BackboneFile.apply(this, arguments);
+
+ this.languages = [];
+}
+util.inherits(Langs, BackboneFile);
+
+Langs.prototype.type = 'langs';
+
+// Parse the readme content
+Langs.prototype.parse = function(content) {
+ var that = this;
+
+ return this.parser.langs(content)
+ .then(function(langs) {
+ that.languages = _.map(langs, function(entry) {
+ return new Language(entry.title, entry.path);
+ });
+ });
+};
+
+// Return the list of languages
+Langs.prototype.list = function() {
+ return this.languages;
+};
+
+// Return default/main language for the book
+Langs.prototype.getDefault = function() {
+ return _.first(this.languages);
+};
+
+// Return true if a language is the default one
+// "lang" cam be a string (id) or a Language entry
+Langs.prototype.isDefault = function(lang) {
+ lang = lang.id || lang;
+ return (this.cound() > 0 && this.getDefault().id == lang);
+};
+
+// Return the count of languages
+Langs.prototype.count = function() {
+ return _.size(this.languages);
+};
+
+// Return templating context for the languages list
+Langs.prototype.getContext = function() {
+ if (this.count() == 0) return {};
+
+ return {
+ languages: {
+ list: _.map(this.languages, function(lang) {
+ return {
+ id: lang.id,
+ title: lang.title
+ };
+ })
+ }
+ };
+};
+
+module.exports = Langs;
diff --git a/lib/backbone/readme.js b/lib/backbone/readme.js
new file mode 100644
index 0000000..a4cd9d8
--- /dev/null
+++ b/lib/backbone/readme.js
@@ -0,0 +1,26 @@
+var util = require('util');
+var BackboneFile = require('./file');
+
+function Readme() {
+ BackboneFile.apply(this, arguments);
+
+ this.title;
+ this.description;
+}
+util.inherits(Readme, BackboneFile);
+
+Readme.prototype.type = 'readme';
+
+// Parse the readme content
+Readme.prototype.parse = function(content) {
+ var that = this;
+
+ return this.parser.readme(content)
+ .then(function(out) {
+ that.title = out.title;
+ that.description = out.description;
+ });
+};
+
+
+module.exports = Readme;
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;