diff options
author | Samy Pessé <samypesse@gmail.com> | 2016-02-26 09:41:26 +0100 |
---|---|---|
committer | Samy Pessé <samypesse@gmail.com> | 2016-02-26 09:41:26 +0100 |
commit | d3d64f636c859f7f01a64f7774cf70bd8ccdc562 (patch) | |
tree | 4f7731f37c3a793d187b0ab1cd77680e69534c6c /lib/backbone | |
parent | 4cb9cbb5ae3aa8f9211ffa3ac5e3d34232c0ca4f (diff) | |
parent | eef072693b17526347c37b66078a5059c71caa31 (diff) | |
download | gitbook-d3d64f636c859f7f01a64f7774cf70bd8ccdc562.zip gitbook-d3d64f636c859f7f01a64f7774cf70bd8ccdc562.tar.gz gitbook-d3d64f636c859f7f01a64f7774cf70bd8ccdc562.tar.bz2 |
Merge pull request #1109 from GitbookIO/3.0.0
Version 3.0.0
Diffstat (limited to 'lib/backbone')
-rw-r--r-- | lib/backbone/file.js | 69 | ||||
-rw-r--r-- | lib/backbone/glossary.js | 99 | ||||
-rw-r--r-- | lib/backbone/index.js | 8 | ||||
-rw-r--r-- | lib/backbone/langs.js | 81 | ||||
-rw-r--r-- | lib/backbone/readme.js | 26 | ||||
-rw-r--r-- | lib/backbone/summary.js | 339 |
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; |