From c8e2fc0e57d223c01a51d6ee186fc1662cd74d13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 21 Jan 2016 16:15:14 +0100
Subject: [PATCH 001/217] Fix typo in config_default.js
---
lib/config_default.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/config_default.js b/lib/config_default.js
index 604003dc9..92defaac0 100644
--- a/lib/config_default.js
+++ b/lib/config_default.js
@@ -6,7 +6,7 @@ module.exports = {
'generator': 'website',
'extension': null,
- // Book metadats (somes are extracted from the README by default)
+ // Book metadatas (somes are extracted from the README by default)
'title': null,
'description': null,
'isbn': null,
@@ -14,7 +14,7 @@ module.exports = {
'direction': null,
'author': null,
- // version of gitbook to use
+ // Version of gitbook to use
'gitbook': '*',
// Structure
From 877f2e477b010f9f37a9044606f110a90f077680 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Fri, 22 Jan 2016 21:04:36 +0100
Subject: [PATCH 002/217] Start rewrite
---
.gitignore | 3 +
lib/backbone/file.js | 61 ++
lib/backbone/glossary.js | 8 +
lib/backbone/index.js | 8 +
lib/backbone/langs.js | 15 +
lib/backbone/readme.js | 26 +
lib/backbone/summary.js | 9 +
lib/blocks.js | 11 -
lib/book.js | 842 +++-----------------
lib/config.js | 110 +++
lib/configuration.js | 210 -----
lib/conrefs_loader.js | 73 --
lib/fs/index.js | 148 ++++
lib/fs/node.js | 72 ++
lib/generator.js | 76 --
lib/generators/ebook.js | 172 ----
lib/generators/index.js | 11 -
lib/generators/json.js | 76 --
lib/generators/website.js | 268 -------
lib/{version.js => gitbook.js} | 4 +-
lib/index.js | 212 +----
lib/init.js | 83 --
lib/logger.js | 123 +++
lib/page.js | 10 +
lib/plugin.js | 241 ------
lib/pluginslist.js | 230 ------
lib/template.js | 466 -----------
lib/utils/batch.js | 52 --
lib/utils/fs.js | 193 -----
lib/utils/git.js | 127 ---
lib/utils/i18n.js | 80 --
lib/utils/images.js | 37 -
lib/utils/links.js | 81 --
lib/utils/logger.js | 102 ---
lib/utils/navigation.js | 79 --
lib/utils/page.js | 397 ---------
lib/utils/path.js | 8 +-
lib/utils/progress.js | 55 --
lib/utils/server.js | 94 ---
lib/utils/string.js | 27 -
lib/utils/watch.js | 40 -
package.json | 4 +-
test/assertions.js | 69 --
test/books/basic/README.md | 3 -
test/books/basic/SUMMARY.md | 1 -
test/books/config-js/README.md | 1 -
test/books/config-js/SUMMARY.md | 1 -
test/books/config-js/book.js | 3 -
test/books/config-json/README.md | 1 -
test/books/config-json/SUMMARY.md | 1 -
test/books/config-json/book.json | 3 -
test/books/conrefs/README.md | 15 -
test/books/conrefs/SUMMARY.md | 1 -
test/books/conrefs/block.md | 1 -
test/books/conrefs/hello.md | 1 -
test/books/glossary/GLOSSARY.md | 13 -
test/books/glossary/README.md | 10 -
test/books/glossary/SUMMARY.md | 3 -
test/books/glossary/folder/PAGE.md | 3 -
test/books/headings/README.md | 3 -
test/books/headings/SUMMARY.md | 0
test/books/highlight/README.md | 16 -
test/books/highlight/SUMMARY.md | 1 -
test/books/images/README.md | 18 -
test/books/images/SUMMARY.md | 3 -
test/books/images/folder/PAGE.md | 3 -
test/books/images/test.svg | 11 -
test/books/init/.gitignore | 3 -
test/books/init/SUMMARY.md | 9 -
test/books/languages/LANGS.md | 4 -
test/books/languages/README.md | 1 -
test/books/languages/en/README.md | 1 -
test/books/languages/en/SUMMARY.md | 1 -
test/books/languages/fr/README.md | 1 -
test/books/languages/fr/SUMMARY.md | 1 -
test/books/links/README.md | 1 -
test/books/links/SUMMARY.md | 4 -
test/books/links/folder1/README.md | 3 -
test/books/links/folder2/README.md | 1 -
test/books/structure/README.adoc | 1 -
test/books/structure/README.md | 1 -
test/books/structure/SUMMARY.md | 1 -
test/books/structure/book.json | 5 -
test/books/structure/glossary.md | 5 -
test/books/style-print/README.md | 3 -
test/books/style-print/SUMMARY.md | 1 -
test/books/style-print/styles/print.css | 3 -
test/books/summary/PAGE1.md | 1 -
test/books/summary/README.md | 1 -
test/books/summary/SUMMARY.md | 5 -
test/books/summary/folder/PAGE2.md | 1 -
test/codehighlighting.js | 65 --
test/config.js | 39 +
test/configuration.js | 37 -
test/conrefs.js | 68 --
test/ebook.js | 70 --
test/format.js | 11 -
test/git.js | 31 -
test/glossary.js | 87 --
test/heading.js | 37 -
test/helper.js | 78 --
test/images.js | 58 --
test/init.js | 24 -
test/json.js | 92 ---
test/languages.js | 37 -
test/links.js | 63 --
test/mock.js | 61 ++
test/navigation.js | 61 --
test/parse.js | 24 +
test/plugins.js | 330 --------
test/plugins/blocks/index.js | 61 --
test/plugins/blocks/package.json | 9 -
test/plugins/config/index.js | 1 -
test/plugins/config/package.json | 21 -
test/plugins/empty/index.js | 1 -
test/plugins/empty/package.json | 9 -
test/plugins/filters/index.js | 10 -
test/plugins/filters/package.json | 9 -
test/plugins/invalid/index.js | 1 -
test/plugins/invalid/package.json | 9 -
test/plugins/replace_highlight/index.js | 14 -
test/plugins/replace_highlight/package.json | 9 -
test/plugins/resources/index.js | 12 -
test/plugins/resources/package.json | 9 -
test/readme.js | 34 +
test/resolve.js | 60 --
test/structure.js | 20 -
test/summary.js | 70 --
test/templating.js | 33 -
test/website.js | 26 -
130 files changed, 865 insertions(+), 6013 deletions(-)
create mode 100644 lib/backbone/file.js
create mode 100644 lib/backbone/glossary.js
create mode 100644 lib/backbone/index.js
create mode 100644 lib/backbone/langs.js
create mode 100644 lib/backbone/readme.js
create mode 100644 lib/backbone/summary.js
delete mode 100644 lib/blocks.js
create mode 100644 lib/config.js
delete mode 100644 lib/configuration.js
delete mode 100644 lib/conrefs_loader.js
create mode 100644 lib/fs/index.js
create mode 100644 lib/fs/node.js
delete mode 100644 lib/generator.js
delete mode 100644 lib/generators/ebook.js
delete mode 100644 lib/generators/index.js
delete mode 100644 lib/generators/json.js
delete mode 100644 lib/generators/website.js
rename lib/{version.js => gitbook.js} (86%)
delete mode 100644 lib/init.js
create mode 100644 lib/logger.js
create mode 100644 lib/page.js
delete mode 100644 lib/plugin.js
delete mode 100644 lib/pluginslist.js
delete mode 100644 lib/template.js
delete mode 100644 lib/utils/batch.js
delete mode 100644 lib/utils/fs.js
delete mode 100644 lib/utils/git.js
delete mode 100644 lib/utils/i18n.js
delete mode 100644 lib/utils/images.js
delete mode 100644 lib/utils/links.js
delete mode 100644 lib/utils/logger.js
delete mode 100644 lib/utils/navigation.js
delete mode 100644 lib/utils/page.js
delete mode 100644 lib/utils/progress.js
delete mode 100644 lib/utils/server.js
delete mode 100644 lib/utils/string.js
delete mode 100644 lib/utils/watch.js
delete mode 100644 test/assertions.js
delete mode 100644 test/books/basic/README.md
delete mode 100644 test/books/basic/SUMMARY.md
delete mode 100644 test/books/config-js/README.md
delete mode 100644 test/books/config-js/SUMMARY.md
delete mode 100644 test/books/config-js/book.js
delete mode 100644 test/books/config-json/README.md
delete mode 100644 test/books/config-json/SUMMARY.md
delete mode 100644 test/books/config-json/book.json
delete mode 100644 test/books/conrefs/README.md
delete mode 100644 test/books/conrefs/SUMMARY.md
delete mode 100644 test/books/conrefs/block.md
delete mode 100644 test/books/conrefs/hello.md
delete mode 100644 test/books/glossary/GLOSSARY.md
delete mode 100644 test/books/glossary/README.md
delete mode 100644 test/books/glossary/SUMMARY.md
delete mode 100644 test/books/glossary/folder/PAGE.md
delete mode 100644 test/books/headings/README.md
delete mode 100644 test/books/headings/SUMMARY.md
delete mode 100644 test/books/highlight/README.md
delete mode 100644 test/books/highlight/SUMMARY.md
delete mode 100644 test/books/images/README.md
delete mode 100644 test/books/images/SUMMARY.md
delete mode 100644 test/books/images/folder/PAGE.md
delete mode 100644 test/books/images/test.svg
delete mode 100644 test/books/init/.gitignore
delete mode 100644 test/books/init/SUMMARY.md
delete mode 100644 test/books/languages/LANGS.md
delete mode 100644 test/books/languages/README.md
delete mode 100644 test/books/languages/en/README.md
delete mode 100644 test/books/languages/en/SUMMARY.md
delete mode 100644 test/books/languages/fr/README.md
delete mode 100644 test/books/languages/fr/SUMMARY.md
delete mode 100644 test/books/links/README.md
delete mode 100644 test/books/links/SUMMARY.md
delete mode 100644 test/books/links/folder1/README.md
delete mode 100644 test/books/links/folder2/README.md
delete mode 100644 test/books/structure/README.adoc
delete mode 100644 test/books/structure/README.md
delete mode 100644 test/books/structure/SUMMARY.md
delete mode 100644 test/books/structure/book.json
delete mode 100644 test/books/structure/glossary.md
delete mode 100644 test/books/style-print/README.md
delete mode 100644 test/books/style-print/SUMMARY.md
delete mode 100644 test/books/style-print/styles/print.css
delete mode 100644 test/books/summary/PAGE1.md
delete mode 100644 test/books/summary/README.md
delete mode 100644 test/books/summary/SUMMARY.md
delete mode 100644 test/books/summary/folder/PAGE2.md
delete mode 100644 test/codehighlighting.js
create mode 100644 test/config.js
delete mode 100644 test/configuration.js
delete mode 100644 test/conrefs.js
delete mode 100644 test/ebook.js
delete mode 100644 test/format.js
delete mode 100644 test/git.js
delete mode 100644 test/glossary.js
delete mode 100644 test/heading.js
delete mode 100644 test/helper.js
delete mode 100644 test/images.js
delete mode 100644 test/init.js
delete mode 100644 test/json.js
delete mode 100644 test/languages.js
delete mode 100644 test/links.js
create mode 100644 test/mock.js
delete mode 100644 test/navigation.js
create mode 100644 test/parse.js
delete mode 100644 test/plugins.js
delete mode 100644 test/plugins/blocks/index.js
delete mode 100644 test/plugins/blocks/package.json
delete mode 100644 test/plugins/config/index.js
delete mode 100644 test/plugins/config/package.json
delete mode 100644 test/plugins/empty/index.js
delete mode 100644 test/plugins/empty/package.json
delete mode 100644 test/plugins/filters/index.js
delete mode 100644 test/plugins/filters/package.json
delete mode 100644 test/plugins/invalid/index.js
delete mode 100644 test/plugins/invalid/package.json
delete mode 100644 test/plugins/replace_highlight/index.js
delete mode 100644 test/plugins/replace_highlight/package.json
delete mode 100644 test/plugins/resources/index.js
delete mode 100644 test/plugins/resources/package.json
create mode 100644 test/readme.js
delete mode 100644 test/resolve.js
delete mode 100644 test/structure.js
delete mode 100644 test/summary.js
delete mode 100644 test/templating.js
delete mode 100644 test/website.js
diff --git a/.gitignore b/.gitignore
index 9550e4fec..4fabbc930 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,6 @@ node_modules
# vim swapfile
*.swp
+
+lib2
+test2
\ No newline at end of file
diff --git a/lib/backbone/file.js b/lib/backbone/file.js
new file mode 100644
index 000000000..71fc78cf9
--- /dev/null
+++ b/lib/backbone/file.js
@@ -0,0 +1,61 @@
+
+function BackboneFile(book) {
+ if (!(this instanceof BackboneFile)) return new BackboneFile(book);
+
+ this.book = book;
+ this.log = this.book.log;
+
+ // Filename in the book
+ this.filename = '';
+ this.parser;
+}
+
+// Type of the backbone file
+BackboneFile.prototype.type = '';
+
+// Parse a backbone file
+BackboneFile.prototype.parse = function() {
+
+};
+
+// Return true if backbone file exists
+BackboneFile.prototype.exists = function() {
+ return Boolean(this.filename);
+};
+
+// 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.filename = 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.filename);
+
+ return this.locate()
+ .then(function() {
+ if (!that.filename) return;
+
+ that.log.debug.ln(that.type, 'located at', that.filename);
+
+ return that.book.readFile(that.filename)
+
+ // 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 000000000..aba83cf3c
--- /dev/null
+++ b/lib/backbone/glossary.js
@@ -0,0 +1,8 @@
+
+function Glossary() {
+ if (!(this instanceof Glossary)) return new Glossary();
+}
+
+Glossary.prototype.type = 'glossary';
+
+module.exports = Glossary;
diff --git a/lib/backbone/index.js b/lib/backbone/index.js
new file mode 100644
index 000000000..4c3c3f342
--- /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 000000000..2818519ef
--- /dev/null
+++ b/lib/backbone/langs.js
@@ -0,0 +1,15 @@
+
+function Langs() {
+ if (!(this instanceof Langs)) return new Langs();
+
+ this.languages = [];
+}
+
+Langs.prototype.type = 'langs';
+
+// Return the count of languages
+Langs.prototype.count = function() {
+ return this.languages.length;
+};
+
+module.exports = Langs;
diff --git a/lib/backbone/readme.js b/lib/backbone/readme.js
new file mode 100644
index 000000000..a4cd9d8a9
--- /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 000000000..e2cd48570
--- /dev/null
+++ b/lib/backbone/summary.js
@@ -0,0 +1,9 @@
+
+function Summary() {
+ if (!(this instanceof Summary)) return new Summary();
+}
+
+Summary.prototype.type = 'summary';
+
+
+module.exports = Summary;
diff --git a/lib/blocks.js b/lib/blocks.js
deleted file mode 100644
index 92097a707..000000000
--- a/lib/blocks.js
+++ /dev/null
@@ -1,11 +0,0 @@
-var _ = require('lodash');
-
-module.exports = {
- // Return non-parsed html
- // since blocks are by default non-parsable, a simple identity method works fine
- html: _.identity,
-
- // Highlight a code block
- // This block can be extent by plugins
- code: _.identity
-};
diff --git a/lib/book.js b/lib/book.js
index 9ba27f319..3bb5bf17f 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -1,25 +1,27 @@
-var Q = require('q');
var _ = require('lodash');
+var Q = require('q');
var path = require('path');
+var Ignore = require('ignore');
var parsers = require('gitbook-parsers');
-var fs = require('./utils/fs');
-var parseNavigation = require('./utils/navigation');
-var parseProgress = require('./utils/progress');
-var pageUtil = require('./utils/page');
+var Logger = require('./logger');
+var Config = require('./config');
+var Readme = require('./backbone/readme');
+var Glossary = require('./backbone/glossary');
+var Summary = require('./backbone/summary');
+var Langs = require('./backbone/langs');
+var Page = require('./page');
var pathUtil = require('./utils/path');
-var links = require('./utils/links');
-var i18n = require('./utils/i18n');
-var logger = require('./utils/logger');
-var Configuration = require('./configuration');
-var TemplateEngine = require('./template');
-var PluginsList = require('./pluginslist');
+function Book(opts) {
+ if (!(this instanceof Book)) return new Book(opts);
-var generators = require('./generators');
+ opts = _.defaults(opts || {}, {
+ fs: null,
+
+ // Root path for the book
+ root: '',
-var Book = function(root, context, parent) {
- this.context = _.defaults(context || {}, {
// Extend book configuration
config: {},
@@ -32,572 +34,116 @@ var Book = function(root, context, parent) {
logLevel: 'info'
});
- // Log
- this.log = logger(this.context.log, this.context.logLevel);
+ if (!opts.fs) throw new Error('Book requires a fs instance');
- // Root folder of the book
- this.root = path.resolve(root);
+ // Root path for the book
+ this.root = opts.root;
- // Parent book
- this.parent = parent;
+ // If multi-lingual, book can have a parent
+ this.parent = opts.parent;
- // Configuration
- this.config = new Configuration(this, this.context.config);
- Object.defineProperty(this, 'options', {
- get: function () {
- return this.config.options;
- }
- });
+ // A book is linked to an fs, to access its content
+ this.fs = opts.fs;
- // Template
- this.template = new TemplateEngine(this);
+ // Rules to ignore some files
+ this.ignore = Ignore();
+ this.ignore.addPattern(['.git', '.svn', '.DS_Store']);
- // Summary
- this.summary = {};
- this.navigation = [];
+ // Create a logger for the book
+ this.log = new Logger(opts.log, opts.logLevel);
- // Glossary
- this.glossary = [];
+ // Create an interface to access the configuration
+ this.config = new Config(this, opts.config);
- // Langs
- this.langs = [];
+ // Interfaces for the book structure
+ this.readme = new Readme(this);
+ this.summary = new Summary(this);
+ this.glossary = new Glossary(this);
+ this.langs = new Langs(this);
- // Sub-books
- this.books = [];
+ // Map of pages in the bok
+ this.pages = {};
- // Files in the book
- this.files = [];
-
- // List of plugins
- this.plugins = new PluginsList(this);
-
- // Structure files
- this.summaryFile = null;
- this.glossaryFile = null;
- this.readmeFile = null;
- this.langsFile = null;
-
- // Bind methods
_.bindAll(this);
+}
+
+// Parse and prepare the configuration, fail if invalid
+Book.prototype.prepareConfig = function() {
+ return this.config.load();
};
-// Return string representation
-Book.prototype.toString = function() {
- return '[Book '+this.root+']';
+// Resolve a path in the book source
+// Enforce that the output path in the root folder
+Book.prototype.resolve = function() {
+ return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments)));
};
-// Initialize and parse the book: config, summary, glossary
+// Parse .gitignore, etc to extract rules
+Book.prototype.parseIgnoreRules = function() {
+ var that = this;
+
+ return _.reduce([
+ '.ignore',
+ '.gitignore',
+ '.bookignore'
+ ], function(prev, filename) {
+ return prev.then(function() {
+ return that.readFile(filename);
+ })
+ .then(function(content) {
+ that.ignore.addPattern(content.toString().split(/\r?\n/));
+ });
+ }, Q());
+};
+
+// Parse the whole book
Book.prototype.parse = function() {
var that = this;
- var multilingual = false;
- return this.parseConfig()
-
- .then(function() {
- return that.parsePlugins();
- })
-
- .then(function() {
- return that.parseLangs()
- .then(function() {
- multilingual = that.langs.length > 0;
- if (multilingual) that.log.info.ln('Parsing multilingual book, with', that.langs.length, 'languages');
-
- // Sub-books that inherit from the current book configuration
- that.books = _.map(that.langs, function(lang) {
- that.log.info.ln('Preparing language book', lang.lang);
- return new Book(
- path.join(that.root, lang.path),
- _.merge({}, that.context, {
- config: _.extend({}, that.options, {
- 'output': path.join(that.options.output, lang.lang),
- 'language': lang.lang
- })
- }),
- that
- );
- });
- });
- })
-
- .then(function() {
- if (multilingual) return;
- return that.listAllFiles();
- })
- .then(function() {
- if (multilingual) return;
- return that.parseReadme();
- })
- .then(function() {
- if (multilingual) return;
- return that.parseSummary();
- })
- .then(function() {
- if (multilingual) return;
- return that.parseGlossary();
- })
-
- .then(function() {
- // Init sub-books
- return _.reduce(that.books, function(prev, book) {
- return prev.then(function() {
- return book.parse();
- });
- }, Q());
- })
-
- .thenResolve(this);
-};
-
-// Generate the output
-Book.prototype.generate = function(generator) {
- var that = this;
- that.options.generator = generator || that.options.generator;
-
- that.log.info.ln('start generation with', that.options.generator, 'generator');
return Q()
+ .then(this.prepareConfig)
+ .then(this.parseIgnoreRules)
- // Clean output folder
+ // Parse languages
.then(function() {
- that.log.info('clean', that.options.generator, 'generator');
- return fs.clean(that.options.output)
- .progress(function(p) {
- that.log.debug.ln('remove', p.file, '('+p.i+'/'+p.count+')');
+ return that.langs.load();
+ })
+
+ .then(function() {
+ if (that.isMultilingual()) return;
+
+ return Q()
+ .then(that.readme.load)
+ .then(function() {
+ if (that.readme.exists()) return;
+
+ throw new Error('No README file (or is ignored)');
})
+
+ .then(that.summary.load)
.then(function() {
- that.log.info.ok();
- });
- })
+ if (that.summary.exists()) return;
- // Create generator
- .then(function() {
- var Generator = generators[generator];
- if (!Generator) throw 'Generator \''+that.options.generator+'\' doesn\'t exist';
- generator = new Generator(that);
+ throw new Error('No SUMMARY file (or is ignored)');
+ })
- return generator.prepare();
- })
-
- // Transform configuration
- .then(function() {
- return that.callHook('config', that.config.dump())
- .then(function(newConfig) {
- that.config.replace(newConfig);
- });
- })
-
- // Generate content
- .then(function() {
- if (that.isMultilingual()) {
- return that.generateMultiLingual(generator);
- } else {
- // Separate list of files into the different operations needed
- var ops = _.groupBy(that.files, function(file) {
- if (file[file.length -1] == '/') {
- return 'directories';
- } else if (_.contains(parsers.extensions, path.extname(file)) && that.navigation[file]) {
- return 'content';
- } else {
- return 'files';
- }
- });
-
-
- return Q()
-
- // First, let's create folder
- .then(function() {
- return _.reduce(ops.directories || [], function(prev, folder) {
- return prev.then(function() {
- that.log.debug.ln('transferring folder', folder);
- return Q(generator.transferFolder(folder));
- });
- }, Q());
- })
-
- // Then, let's copy other files
- .then(function() {
- return Q.all(_.map(ops.files || [], function(file) {
- that.log.debug.ln('transferring file', file);
- return Q(generator.transferFile(file));
- }));
- })
-
- // Finally let's generate content
- .then(function() {
- var nFiles = (ops.content || []).length;
- return _.reduce(ops.content || [], function(prev, file, i) {
- return prev.then(function() {
- var p = ((i*100)/nFiles).toFixed(0)+'%';
- that.log.debug.ln('processing', file, p);
-
- return Q(generator.convertFile(file))
- .fail(function(err) {
- // Transform error message to signal file
- throw that.normError(err, {
- fileName: file
- });
- });
- });
- }, Q());
- });
- }
- })
-
- // Finish generation
- .then(function() {
- return that.callHook('finish:before');
- })
- .then(function() {
- return generator.finish();
- })
- .then(function() {
- return that.callHook('finish');
- })
- .then(function() {
- that.log.info.ln('generation is finished');
+ .then(that.glossary.load);
});
};
-// Generate the output for a multilingual book
-Book.prototype.generateMultiLingual = function() {
- var that = this;
-
- return Q()
- .then(function() {
- // Generate sub-books
- return _.reduce(that.books, function(prev, book) {
- return prev.then(function() {
- return book.generate(that.options.generator);
- });
- }, Q());
- });
+// Test if a file is ignored, return true if it is
+Book.prototype.isFileIgnored = function(filename) {
+ return this.ignore.filter([filename]).length == 0;
};
-// Extract files from ebook generated
-Book.prototype.generateFile = function(output, options) {
- var book = this;
-
- options = _.defaults(options || {}, {
- ebookFormat: path.extname(output).slice(1)
- });
- output = output || path.resolve(book.root, 'book.'+options.ebookFormat);
-
- return fs.tmp.dir()
- .then(function(tmpDir) {
- book.setOutput(tmpDir);
-
- return book.generate(options.ebookFormat)
- .then(function() {
- var copyFile = function(lang) {
- var _outputFile = output;
- var _tmpDir = tmpDir;
-
- if (lang) {
- _outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+'_'+lang+path.extname(_outputFile);
- _tmpDir = path.join(_tmpDir, lang);
- }
-
- book.log.debug.ln('copy ebook to', _outputFile);
- return fs.copy(
- path.join(_tmpDir, 'index.'+options.ebookFormat),
- _outputFile
- );
- };
-
- // Multi-langs book
- return Q()
- .then(function() {
- if (book.isMultilingual()) {
- return Q.all(
- _.map(book.langs, function(lang) {
- return copyFile(lang.lang);
- })
- )
- .thenResolve(book.langs.length);
- } else {
- return copyFile().thenResolve(1);
- }
- })
- .then(function(n) {
- book.log.info.ok(n+' file(s) generated');
-
- return fs.remove(tmpDir);
- });
- });
- });
+// Read a file in the book, throw error if ignored
+Book.prototype.readFile = function(filename) {
+ if (this.isFileIgnored(filename)) return Q.reject(new Error('File "'+filename+'" is ignored'));
+ return this.fs.readAsString(this.resolve(filename));
};
-// Parse configuration
-Book.prototype.parseConfig = function() {
- var that = this;
-
- that.log.info('loading book configuration....');
- return that.config.load()
- .then(function() {
- that.log.info.ok();
- });
-};
-
-// Parse list of plugins
-Book.prototype.parsePlugins = function() {
- var that = this;
-
- // Load plugins
- return that.plugins.load(that.options.plugins)
- .then(function() {
- if (_.size(that.plugins.failed) > 0) return Q.reject(new Error('Error loading plugins: '+that.plugins.failed.join(',')+'. Run \'gitbook install\' to install plugins from NPM.'));
-
- that.log.info.ok(that.plugins.count()+' plugins loaded');
- that.log.debug.ln('normalize plugins list');
- });
-};
-
-// Parse readme to extract defaults title and description
-Book.prototype.parseReadme = function() {
- var that = this;
- var filename = that.config.getStructure('readme', true);
- that.log.debug.ln('start parsing readme:', filename);
-
- return that.findFile(filename)
- .then(function(readme) {
- if (!readme) throw 'No README file';
- if (!_.contains(that.files, readme.path)) throw 'README file is ignored';
-
- that.readmeFile = readme.path;
- that._defaultsStructure(that.readmeFile);
-
- that.log.debug.ln('readme located at', that.readmeFile);
- return that.template.renderFile(that.readmeFile)
- .then(function(content) {
- return readme.parser.readme(content)
- .fail(function(err) {
- throw that.normError(err, {
- name: err.name || 'Readme Parse Error',
- fileName: that.readmeFile
- });
- });
- });
- })
- .then(function(readme) {
- that.options.title = that.options.title || readme.title;
- that.options.description = that.options.description || readme.description;
- });
-};
-
-
-// Parse langs to extract list of sub-books
-Book.prototype.parseLangs = function() {
- var that = this;
-
- var filename = that.config.getStructure('langs', true);
- that.log.debug.ln('start parsing languages index:', filename);
-
- return that.findFile(filename)
- .then(function(langs) {
- if (!langs) return [];
-
- that.langsFile = langs.path;
- that._defaultsStructure(that.langsFile);
-
- that.log.debug.ln('languages index located at', that.langsFile);
- return that.template.renderFile(that.langsFile)
- .then(function(content) {
- return langs.parser.langs(content)
- .fail(function(err) {
- throw that.normError(err, {
- name: err.name || 'Langs Parse Error',
- fileName: that.langsFile
- });
- });
- });
- })
- .then(function(langs) {
- that.langs = langs;
- });
-};
-
-// Parse summary to extract list of chapters
-Book.prototype.parseSummary = function() {
- var that = this;
-
- var filename = that.config.getStructure('summary', true);
- that.log.debug.ln('start parsing summary:', filename);
-
- return that.findFile(filename)
- .then(function(summary) {
- if (!summary) throw 'No SUMMARY file';
-
- // Remove the summary from the list of files to parse
- that.summaryFile = summary.path;
- that._defaultsStructure(that.summaryFile);
- that.files = _.without(that.files, that.summaryFile);
-
- that.log.debug.ln('summary located at', that.summaryFile);
- return that.template.renderFile(that.summaryFile)
- .then(function(content) {
- return summary.parser.summary(content, {
- entryPoint: that.readmeFile,
- entryPointTitle: that.i18n('SUMMARY_INTRODUCTION'),
- files: that.files
- })
- .fail(function(err) {
- throw that.normError(err, {
- name: err.name || 'Summary Parse Error',
- fileName: that.summaryFile
- });
- });
- });
- })
- .then(function(summary) {
- that.summary = summary;
- that.navigation = parseNavigation(that.summary, that.files);
- });
-};
-
-// Parse glossary to extract terms
-Book.prototype.parseGlossary = function() {
- var that = this;
-
- var filename = that.config.getStructure('glossary', true);
- that.log.debug.ln('start parsing glossary: ', filename);
-
- return that.findFile(filename)
- .then(function(glossary) {
- if (!glossary) return [];
-
- // Remove the glossary from the list of files to parse
- that.glossaryFile = glossary.path;
- that._defaultsStructure(that.glossaryFile);
- that.files = _.without(that.files, that.glossaryFile);
-
- that.log.debug.ln('glossary located at', that.glossaryFile);
- return that.template.renderFile(that.glossaryFile)
- .then(function(content) {
- return glossary.parser.glossary(content)
- .fail(function(err) {
- throw that.normError(err, {
- name: err.name || 'Glossary Parse Error',
- fileName: that.glossaryFile
- });
- });
- });
- })
- .then(function(glossary) {
- that.glossary = glossary;
- });
-};
-
-// Parse a page
-Book.prototype.parsePage = function(filename, options) {
- var that = this, page = {};
- options = _.defaults(options || {}, {
- // Transform svg images
- convertImages: false,
-
- // Interpolate before templating
- interpolateTemplate: _.identity,
-
- // Interpolate after templating
- interpolateContent: _.identity
- });
-
- var interpolate = function(fn) {
- return Q(fn(page))
- .then(function(_page) {
- page = _page || page;
- });
- };
-
- that.log.debug.ln('start parsing file', filename);
-
- var extension = path.extname(filename);
- var filetype = parsers.get(extension);
-
- if (!filetype) return Q.reject(new Error('Can\'t parse file: '+filename));
-
- // Type of parser used
- page.type = filetype.name;
-
- // Path relative to book
- page.path = filename;
-
- // Path absolute in the system
- page.rawPath = path.resolve(that.root, filename);
-
- // Progress in the book
- page.progress = parseProgress(that.navigation, filename);
-
- that.log.debug.ln('render template', filename);
-
- // Read file content
- return that.readFile(page.path)
- .then(function(content) {
- page.content = content;
-
- return interpolate(options.interpolateTemplate);
- })
-
- // Prepare page markup
- .then(function() {
- return filetype.page.prepare(page.content)
- .then(function(content) {
- page.content = content;
- });
- })
-
- // Generate template
- .then(function() {
- return that.template.renderPage(page);
- })
-
- // Prepare and Parse markup
- .then(function(content) {
- page.content = content;
-
- that.log.debug.ln('use file parser', filetype.name, 'for', filename);
- return filetype.page(page.content);
- })
-
- // Post process sections
- .then(function(_page) {
- return _.reduce(_page.sections, function(prev, section) {
- return prev.then(function(_sections) {
- return that.template.postProcess(section.content || '')
- .then(function(content) {
- section.content = content;
- return _sections.concat([section]);
- });
- });
- }, Q([]));
- })
-
- // Prepare html
- .then(function(_sections) {
- return pageUtil.normalize(_sections, {
- book: that,
- convertImages: options.convertImages,
- input: filename,
- navigation: that.navigation,
- base: path.dirname(filename) || './',
- output: path.dirname(filename) || './',
- glossary: that.glossary
- });
- })
-
- // Interpolate output
- .then(function(_sections) {
- page.sections = _sections;
- return interpolate(options.interpolateContent);
- })
-
- .then(function() {
- return page;
- });
-};
-
-// Find file that can be parsed with a specific filename
-Book.prototype.findFile = function(filename) {
+// Find a parsable file using a filename
+Book.prototype.findParsableFile = function(filename) {
var that = this;
var ext = path.extname(filename);
@@ -614,7 +160,7 @@ Book.prototype.findFile = function(filename) {
var filepath = basename+ext;
- return fs.findFile(that.root, filepath)
+ return that.fs.findFile(that.root, filepath)
.then(function(realFilepath) {
if (!realFilepath) return null;
@@ -627,197 +173,15 @@ Book.prototype.findFile = function(filename) {
}, Q(null));
};
-// Format a string using a specific markup language
-Book.prototype.formatString = function(extension, content) {
- return Q()
- .then(function() {
- var filetype = parsers.get(extension);
- if (!filetype) throw new Error('Filetype doesn\'t exist: '+filetype);
-
- return filetype.page(content);
- })
-
- // Merge sections
- .then(function(page) {
- return _.reduce(page.sections, function(content, section) {
- return content + section.content;
- }, '');
- });
+// Return true if book is associated to a language
+Book.prototype.isLanguageBook = function() {
+ return Boolean(this.parent);
};
+Book.prototype.isSubBook = Book.prototype.isLanguageBook;
-// Check if a file exists in the book
-Book.prototype.fileExists = function(filename) {
- return fs.exists(
- this.resolve(filename)
- );
-};
-
-// Check if a file path is inside the book
-Book.prototype.fileIsInBook = function(filename) {
- return pathUtil.isInRoot(this.root, filename);
-};
-
-// Read a file
-Book.prototype.readFile = function(filename) {
- return fs.readFile(
- this.resolve(filename),
- { encoding: 'utf8' }
- );
-};
-
-// Return stat for a file
-Book.prototype.statFile = function(filename) {
- return fs.stat(this.resolve(filename));
-};
-
-// List all files in the book
-Book.prototype.listAllFiles = function() {
- var that = this;
-
- return fs.list(this.root, {
- ignoreFiles: ['.ignore', '.gitignore', '.bookignore'],
- ignoreRules: [
- // Skip Git stuff
- '.git/',
- '.gitignore',
-
- // Skip OS X meta data
- '.DS_Store',
-
- // Skip stuff installed by plugins
- 'node_modules',
-
- // Skip book outputs
- '_book',
- '*.pdf',
- '*.epub',
- '*.mobi',
-
- // Skip config files
- '.ignore',
- '.bookignore',
- 'book.json',
- ]
- })
- .then(function(_files) {
- that.files = _files;
- });
-};
-
-// Return true if the book is a multilingual book
+// Return true if the book is main instance of a multilingual book
Book.prototype.isMultilingual = function() {
- return this.books.length > 0;
+ return this.langs.count() > 0;
};
-// Return root of the parent
-Book.prototype.parentRoot = function() {
- if (this.parent) return this.parent.parentRoot();
- return this.root;
-};
-
-// Return true if it's a sub-book
-Book.prototype.isSubBook = function() {
- return !!this.parent;
-};
-
-// Test if the file is the entry point
-Book.prototype.isEntryPoint = function(fp) {
- return fp == this.readmeFile;
-};
-
-// Alias to book.config.get
-Book.prototype.getConfig = function(key, def) {
- return this.config.get(key, def);
-};
-
-// Resolve a path in the book source
-// Enforce that the output path in the root folder
-Book.prototype.resolve = function() {
- return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments)));
-};
-
-// Resolve a path in the book output
-// Enforce that the output path in the output folder
-Book.prototype.resolveOutput = function() {
- return pathUtil.resolveInRoot.apply(null, [this.options.output].concat(_.toArray(arguments)));
-};
-
-// Convert an abslute path into a relative path to this
-Book.prototype.relative = function(p) {
- return path.relative(this.root, p);
-};
-
-// Normalize a path to .html and convert README -> index
-Book.prototype.contentPath = function(link) {
- if (
- path.basename(link, path.extname(link)) == 'README' ||
- link == this.readmeFile
- ) {
- link = path.join(path.dirname(link), 'index'+path.extname(link));
- }
-
- link = links.changeExtension(link, '.html');
- return link;
-};
-
-// Normalize a link to .html and convert README -> index
-Book.prototype.contentLink = function(link) {
- return links.normalize(this.contentPath(link));
-};
-
-// Default structure paths to an extension
-Book.prototype._defaultsStructure = function(filename) {
- var that = this;
- var extension = path.extname(filename);
-
- that.readmeFile = that.readmeFile || that.config.getStructure('readme')+extension;
- that.summaryFile = that.summaryFile || that.config.getStructure('summary')+extension;
- that.glossaryFile = that.glossaryFile || that.config.getStructure('glossary')+extension;
- that.langsFile = that.langsFile || that.config.getStructure('langs')+extension;
-};
-
-// Change output path
-Book.prototype.setOutput = function(p) {
- var that = this;
- this.options.output = path.resolve(p);
-
- _.each(this.books, function(book) {
- book.setOutput(path.join(that.options.output, book.options.language));
- });
-};
-
-// Translate a strign according to the book language
-Book.prototype.i18n = function() {
- var args = Array.prototype.slice.call(arguments);
- return i18n.__.apply({}, [this.config.normalizeLanguage()].concat(args));
-};
-
-// Normalize error
-Book.prototype.normError = function(err, opts, defs) {
- if (_.isString(err)) err = new Error(err);
-
- // Extend err
- _.extend(err, opts || {});
- _.defaults(err, defs || {});
-
- err.lineNumber = err.lineNumber || err.lineno;
- err.columnNumber = err.columnNumber || err.colno;
-
- err.toString = function() {
- var attributes = [];
-
- if (this.fileName) attributes.push('In file \''+this.fileName+'\'');
- if (this.lineNumber) attributes.push('Line '+this.lineNumber);
- if (this.columnNumber) attributes.push('Column '+this.columnNumber);
- return (this.name || 'Error')+': '+this.message+((attributes.length > 0)? ' ('+attributes.join(', ')+')' : '');
- };
-
- return err;
-};
-
-// Call a hook in plugins
-Book.prototype.callHook = function(name, data) {
- return this.plugins.hook(name, data);
-};
-
-module.exports= Book;
+module.exports = Book;
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 000000000..c6d05dae6
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,110 @@
+var Q = require('q');
+var _ = require('lodash');
+var semver = require('semver');
+var path = require('path');
+
+var gitbook = require('./gitbook');
+var configDefault = require('./config_default');
+
+/*
+Config is an interface for the book's configuration stored in "book.json" (or "book.js")
+*/
+
+function Config(book, baseConfig) {
+ this.book = book;
+ this.fs = book.fs;
+ this.log = book.log;
+
+ this.replace(baseConfig || {});
+}
+
+// Load configuration of the book
+// and verify that the configuration is satisfying
+Config.prototype.load = function() {
+ var that = this;
+
+ this.log.debug.ln('loading configuration');
+ return this.fs.loadAsObject(this.book.resolve('book'))
+ .fail(function(err) {
+ if (err.code != 'MODULE_NOT_FOUND') throw(err);
+ else return Q({});
+ })
+ .then(function(_config) {
+ return that.replace(_config);
+ })
+ .then(function() {
+ if (!that.book.isLanguageBook()) {
+ if (!gitbook.satisfies(that.options.gitbook)) {
+ throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook);
+ }
+ if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(gitbook.version, 'patch'), that.options.gitbook)) {
+ that.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(gitbook.version.split('.'))+'.x.x')+'\' is more adequate');
+ }
+ }
+
+ //that.options.output = path.resolve(that.options.output || that.book.resolve('_book'));
+ //that.options.plugins = normalizePluginsList(that.options.plugins);
+ //that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false);
+ //that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
+ //that.options.plugins = _.uniq(that.options.plugins, 'name');
+
+ // Default value for text direction (from language)
+ /*if (!that.options.direction) {
+ var lang = i18n.getCatalog(that.options.language);
+ if (lang) that.options.direction = lang.direction;
+ }*/
+
+ that.options.gitbook = gitbook.version;
+ });
+};
+
+// Replace the whole configuration
+Config.prototype.replace = function(options) {
+ var that = this;
+
+ this.options = _.cloneDeep(configDefault);
+ this.options = _.merge(this.options, options || {});
+
+ // options.input == book.root
+ Object.defineProperty(this.options, 'input', {
+ get: function () {
+ return that.book.root;
+ }
+ });
+
+ // options.originalInput == book.parent.root
+ Object.defineProperty(this.options, 'originalInput', {
+ get: function () {
+ return that.book.parent? that.book.parent.root : undefined;
+ }
+ });
+
+ // options.originalOutput == book.parent.options.output
+ Object.defineProperty(this.options, 'originalOutput', {
+ get: function () {
+ return that.book.parent? that.book.parent.options.output : undefined;
+ }
+ });
+};
+
+// Return path to a structure file
+// Strip the extension by default
+Config.prototype.getStructure = function(name, dontStripExt) {
+ var filename = this.options.structure[name];
+ if (dontStripExt) return filename;
+
+ filename = filename.split('.').slice(0, -1).join('.');
+ return filename;
+};
+
+// Return a configuration using a key and a default value
+Config.prototype.get = function(key, def) {
+ return _.get(this.options, key, def);
+};
+
+// Update a configuration
+Config.prototype.set = function(key, value) {
+ return _.set(this.options, key, value);
+};
+
+module.exports = Config;
diff --git a/lib/configuration.js b/lib/configuration.js
deleted file mode 100644
index dd955851d..000000000
--- a/lib/configuration.js
+++ /dev/null
@@ -1,210 +0,0 @@
-var _ = require('lodash');
-var Q = require('q');
-var path = require('path');
-var semver = require('semver');
-
-var pkg = require('../package.json');
-var i18n = require('./utils/i18n');
-var version = require('./version');
-
-var DEFAULT_CONFIG = require('./config_default');
-
-// Default plugins added to each books
-var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings'];
-
-// Check if a plugin is a default plugin
-// Plugin should be in the list
-// And version from book.json specified for this plugin should be satisfied
-function isDefaultPlugin(name, version) {
- if (!_.contains(DEFAULT_PLUGINS, name)) return false;
-
- try {
- var pluginPkg = require('gitbook-plugin-'+name+'/package.json');
- return semver.satisfies(pluginPkg.version, version || '*');
- } catch(e) {
- return false;
- }
-}
-
-// Normalize a list of plugins to use
-function normalizePluginsList(plugins, addDefaults) {
- // Normalize list to an array
- plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []);
-
- // Remove empty parts
- plugins = _.compact(plugins);
-
- // Divide as {name, version} to handle format like 'myplugin@1.0.0'
- plugins = _.map(plugins, function(plugin) {
- if (plugin.name) return plugin;
-
- var parts = plugin.split('@');
- var name = parts[0];
- var version = parts[1];
- return {
- 'name': name,
- 'version': version, // optional
- 'isDefault': isDefaultPlugin(name, version)
- };
- });
-
- // List plugins to remove
- var toremove = _.chain(plugins)
- .filter(function(plugin) {
- return plugin.name.length > 0 && plugin.name[0] == '-';
- })
- .map(function(plugin) {
- return plugin.name.slice(1);
- })
- .value();
-
- // Merge with defaults
- if (addDefaults !== false) {
- _.each(DEFAULT_PLUGINS, function(plugin) {
- if (_.find(plugins, { name: plugin })) {
- return;
- }
-
- plugins.push({
- 'name': plugin,
- 'isDefault': true
- });
- });
- }
-
- // Remove plugin that start with '-'
- plugins = _.filter(plugins, function(plugin) {
- return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-');
- });
-
- // Remove duplicates
- plugins = _.uniq(plugins, 'name');
-
- return plugins;
-}
-
-var Configuration = function(book, options) {
- this.book = book;
- this.replace(options);
-};
-
-// Read and parse the configuration
-Configuration.prototype.load = function() {
- var that = this;
-
- return Q()
- .then(function() {
- var configPath, _config;
-
- try {
- configPath = require.resolve(
- that.book.resolve(that.options.configFile)
- );
-
- // Invalidate node.js cache for livreloading
- delete require.cache[configPath];
-
- _config = require(configPath);
- that.options = _.merge(
- that.options,
- _.omit(_config, 'configFile', 'defaultsPlugins', 'generator', 'extension')
- );
- }
- catch(err) {
- if (err instanceof SyntaxError) return Q.reject(err);
- return Q();
- }
- })
- .then(function() {
- if (!that.book.isSubBook()) {
- if (!version.satisfies(that.options.gitbook)) {
- throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook);
- }
- if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(pkg.version, 'patch'), that.options.gitbook)) {
- that.book.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(pkg.version.split('.'))+'.x.x')+'\' is more adequate');
- }
- }
-
- that.options.output = path.resolve(that.options.output || that.book.resolve('_book'));
- that.options.plugins = normalizePluginsList(that.options.plugins);
- that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false);
- that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
- that.options.plugins = _.uniq(that.options.plugins, 'name');
-
- // Default value for text direction (from language)
- if (!that.options.direction) {
- var lang = i18n.getCatalog(that.options.language);
- if (lang) that.options.direction = lang.direction;
- }
-
- that.options.gitbook = pkg.version;
- });
-};
-
-// Extend the configuration
-Configuration.prototype.extend = function(options) {
- _.extend(this.options, options);
-};
-
-// Replace the whole configuration
-Configuration.prototype.replace = function(options) {
- var that = this;
-
- this.options = _.cloneDeep(DEFAULT_CONFIG);
- this.options = _.merge(this.options, options || {});
-
- // options.input == book.root
- Object.defineProperty(this.options, 'input', {
- get: function () {
- return that.book.root;
- }
- });
-
- // options.originalInput == book.parent.root
- Object.defineProperty(this.options, 'originalInput', {
- get: function () {
- return that.book.parent? that.book.parent.root : undefined;
- }
- });
-
- // options.originalOutput == book.parent.options.output
- Object.defineProperty(this.options, 'originalOutput', {
- get: function () {
- return that.book.parent? that.book.parent.options.output : undefined;
- }
- });
-};
-
-// Dump configuration as json object
-Configuration.prototype.dump = function() {
- return _.cloneDeep(this.options);
-};
-
-// Get structure file
-Configuration.prototype.getStructure = function(name, dontStripExt) {
- var filename = this.options.structure[name];
- if (dontStripExt) return filename;
-
- filename = filename.split('.').slice(0, -1).join('.');
- return filename;
-};
-
-// Return normalized language
-Configuration.prototype.normalizeLanguage = function() {
- return i18n.normalizeLanguage(this.options.language);
-};
-
-// Return a configuration
-Configuration.prototype.get = function(key, def) {
- return _.get(this.options, key, def);
-};
-
-// Update a configuration
-Configuration.prototype.set = function(key, value) {
- return _.set(this.options, key, value);
-};
-
-// Default configuration
-Configuration.DEFAULT = DEFAULT_CONFIG;
-
-module.exports= Configuration;
diff --git a/lib/conrefs_loader.js b/lib/conrefs_loader.js
deleted file mode 100644
index 255bf0647..000000000
--- a/lib/conrefs_loader.js
+++ /dev/null
@@ -1,73 +0,0 @@
-var _ = require('lodash');
-var path = require('path');
-var nunjucks = require('nunjucks');
-
-var git = require('./utils/git');
-var fs = require('./utils/fs');
-var pathUtil = require('./utils/path');
-
-// The loader should handle relative and git url
-var BookLoader = nunjucks.Loader.extend({
- async: true,
-
- init: function(book, opts) {
- this.opts = _.defaults(opts || {}, {
- interpolate: _.identity
- });
- this.book = book;
- },
-
- getSource: function(fileurl, callback) {
- var that = this;
-
- git.resolveFile(fileurl)
- .then(function(filepath) {
- // Is local file
- if (!filepath) filepath = path.resolve(fileurl);
- else that.book.log.debug.ln('resolve from git', fileurl, 'to', filepath);
-
- // Read file from absolute path
- return fs.readFile(filepath)
- .then(function(source) {
- return that.opts.interpolate(filepath, source.toString());
- })
- .then(function(source) {
- return {
- src: source,
- path: filepath,
-
- // We disable cache sincde content is modified (shortcuts, ...)
- noCache: true
- };
- });
- })
- .nodeify(callback);
- },
-
- resolve: function(from, to) {
- // If origin is in the book, we enforce result file to be in the book
- if (this.book.fileIsInBook(from)) {
- return this.book.resolve(
- this.book.relative(path.dirname(from)),
- to
- );
- }
-
- // If origin is in a git repository, we resolve file in the git repository
- var gitRoot = git.resolveRoot(from);
- if (gitRoot) {
- return pathUtil.resolveInRoot(gitRoot, to);
- }
-
- // If origin is not in the book (include from a git content ref)
- return path.resolve(path.dirname(from), to);
- },
-
- // Handle all files as relative, so that nunjucks pass responsability to 'resolve'
- // Only git urls are considered as absolute
- isRelative: function(filename) {
- return !git.checkUrl(filename);
- }
-});
-
-module.exports = BookLoader;
diff --git a/lib/fs/index.js b/lib/fs/index.js
new file mode 100644
index 000000000..1961b07e2
--- /dev/null
+++ b/lib/fs/index.js
@@ -0,0 +1,148 @@
+var Q = require('q');
+var _ = require('lodash');
+var path = require('path');
+var Buffer = require('buffer').Buffer;
+var destroy = require('destroy');
+
+/*
+A filesystem is an interface to read/write files
+GitBook can works with a virtual filesystem, for example in the browser.
+*/
+
+// .readdir return files/folder as a list of string, folder ending with '/'
+function pathIsFolder(filename) {
+ return _.last(filename) == '/' || _.last(filename) == '\\';
+}
+
+
+function FS() {
+
+}
+
+// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
+FS.prototype.exists = function(filename) {
+ // To implement for each fs
+};
+
+// Read a file and returns a promise with the content as a buffer
+FS.prototype.read = function(filename) {
+ // To implement for each fs
+};
+
+// Write a file and returns a promise
+FS.prototype.write = function(filename, buffer) {
+ // To implement for each fs
+};
+
+// List files/directories in a directory
+FS.prototype.readdir = function(folder) {
+ // To implement for each fs
+};
+
+
+// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
+// For optmization, it can be redefined:
+
+// List files in a directory
+FS.prototype.listFiles = function(folder) {
+ return this.readdir(folder)
+ .then(function(files) {
+ return _.reject(files, pathIsFolder);
+ });
+};
+
+// List all files in the fs
+FS.prototype.listAllFiles = function(folder) {
+ var that = this;
+
+ return this.readdir(folder)
+ .then(function(files) {
+ return _.reduce(files, function(prev, file) {
+ return prev.then(function(output) {
+ var isDirectory = pathIsFolder(file);
+
+ if (!isDirectory) {
+ output.push(file);
+ return output;
+ } else {
+ return that.listAllFiles(path.join(folder, file))
+ .then(function(files) {
+ return output.concat(_.map(files, function(_file) {
+ return path.join(file, _file);
+ }));
+ });
+ }
+ });
+ }, Q([]));
+ });
+};
+
+// Read a file as a string (utf-8)
+FS.prototype.readAsString = function(filename) {
+ return this.read(filename)
+ .then(function(buf) {
+ return buf.toString('utf-8');
+ });
+};
+
+// Write a stream to a file and returns a promise
+FS.prototype.writeStream = function(filename, stream) {
+ var bufs = [];
+ var d = Q.defer();
+
+ var cleanup = function() {
+ destroy(stream);
+ stream.removeAllListeners();
+ };
+
+ stream.on('data', function(d) {
+ bufs.push(d);
+ });
+
+ stream.on('error', function(err) {
+ cleanup();
+
+ d.reject(err);
+ });
+
+ stream.on('end', function(){
+ cleanup();
+
+ var buf = Buffer.concat(bufs);
+ d.resolve(buf);
+ });
+
+ return d.promise;
+};
+
+// Copy a file
+FS.prototype.copy = function(from, to) {
+ var that = this;
+
+ return this.read(from)
+ .then(function(buf) {
+ return that.write(to, buf);
+ });
+};
+
+// Find a file in a folder (case incensitive)
+// Return the real filename
+FS.prototype.findFile = function findFile(root, filename) {
+ return this.listFiles(root)
+ .then(function(files) {
+ return _.find(files, function(file) {
+ return (file.toLowerCase() == filename.toLowerCase());
+ });
+ });
+};
+
+// Load a JSON file
+// By default, fs only supports JSON
+FS.prototype.loadAsObject = function(filename) {
+ return this.readAsString(filename)
+ .then(function(str) {
+ return JSON.parse(str);
+ });
+};
+
+module.exports = FS;
diff --git a/lib/fs/node.js b/lib/fs/node.js
new file mode 100644
index 000000000..0c470d728
--- /dev/null
+++ b/lib/fs/node.js
@@ -0,0 +1,72 @@
+var Q = require('q');
+var _ = require('lodash');
+var util = require('util');
+var path = require('path');
+var fs = require('fs');
+
+var BaseFS = require('./');
+
+function NodeFS() {
+ BaseFS.call(this);
+}
+util.inherits(NodeFS, BaseFS);
+
+// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
+NodeFS.prototype.exists = function(filename) {
+ var d = Q.defer();
+
+ fs.exists(filename, function(exists) {
+ d.resolve(exists);
+ });
+
+ return d.promise;
+};
+
+// Read a file and returns a promise with the content as a buffer
+NodeFS.prototype.read = function(filename) {
+ return Q.nfcall(fs.readFile, filename);
+};
+
+// Write a file and returns a promise
+NodeFS.prototype.write = function(filename, buffer) {
+ return Q.nfcall(fs.writeFile, filename, buffer);
+};
+
+// List files in a directory
+NodeFS.prototype.readdir = function(folder) {
+ return Q.nfcall(fs.readdir, folder)
+ .then(function(files) {
+ return _.chain(files)
+ .map(function(file) {
+ if (file == '.' || file == '..') return;
+
+ var stat = fs.statSync(path.join(folder, file));
+ if (stat.isDirectory()) file = file + path.sep;
+ return file;
+ })
+ .compact()
+ .value();
+ });
+};
+
+// Load a JSON/JS file
+NodeFS.prototype.loadAsObject = function(filename) {
+ return Q()
+ .then(function() {
+ var jsFile;
+
+ try {
+ jsFile = require.resolve(filename);
+
+ // Invalidate node.js cache for livreloading
+ delete require.cache[jsFile];
+
+ return require(jsFile);
+ }
+ catch(err) {
+ return Q.reject(err);
+ }
+ });
+};
+
+module.exports = NodeFS;
diff --git a/lib/generator.js b/lib/generator.js
deleted file mode 100644
index 4e280d81b..000000000
--- a/lib/generator.js
+++ /dev/null
@@ -1,76 +0,0 @@
-var _ = require('lodash');
-var path = require('path');
-var Q = require('q');
-var fs = require('./utils/fs');
-
-var BaseGenerator = function(book) {
- this.book = book;
-
- Object.defineProperty(this, 'options', {
- get: function () {
- return this.book.options;
- }
- });
-
- _.bindAll(this);
-};
-
-BaseGenerator.prototype.callHook = function(name, data) {
- return this.book.callHook(name, data);
-};
-
-// Prepare the genertor
-BaseGenerator.prototype.prepare = function() {
- var that = this;
-
- return that.callHook('init');
-};
-
-// Write a parsed file to the output
-BaseGenerator.prototype.convertFile = function(input) {
- return Q.reject(new Error('Could not convert '+input));
-};
-
-// Copy file to the output (non parsable)
-BaseGenerator.prototype.transferFile = function(input) {
- return fs.copy(
- this.book.resolve(input),
- path.join(this.options.output, input)
- );
-};
-
-// Copy a folder to the output
-BaseGenerator.prototype.transferFolder = function(input) {
- return fs.mkdirp(
- path.join(this.book.options.output, input)
- );
-};
-
-// Copy the cover picture
-BaseGenerator.prototype.copyCover = function() {
- var that = this;
-
- return Q.all([
- fs.copy(that.book.resolve('cover.jpg'), path.join(that.options.output, 'cover.jpg')),
- fs.copy(that.book.resolve('cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg'))
- ])
- .fail(function() {
- // If orignaly from multi-lang, try copy from parent
- if (!that.book.isSubBook()) return;
-
- return Q.all([
- fs.copy(path.join(that.book.parentRoot(), 'cover.jpg'), path.join(that.options.output, 'cover.jpg')),
- fs.copy(path.join(that.book.parentRoot(), 'cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg'))
- ]);
- })
- .fail(function() {
- return Q();
- });
-};
-
-// At teh end of the generation
-BaseGenerator.prototype.finish = function() {
- return Q.reject(new Error('Could not finish generation'));
-};
-
-module.exports = BaseGenerator;
diff --git a/lib/generators/ebook.js b/lib/generators/ebook.js
deleted file mode 100644
index ff804c696..000000000
--- a/lib/generators/ebook.js
+++ /dev/null
@@ -1,172 +0,0 @@
-var util = require('util');
-var path = require('path');
-var Q = require('q');
-var _ = require('lodash');
-var juice = require('juice');
-var exec = require('child_process').exec;
-
-var fs = require('../utils/fs');
-var stringUtils = require('../utils/string');
-var BaseGenerator = require('./website');
-
-var Generator = function(book, format) {
- BaseGenerator.apply(this, arguments);
-
- // eBook format
- this.ebookFormat = format;
-
- // Resources namespace
- this.namespace = 'ebook';
-
- // Styles to use
- this.styles = _.compact(['print', 'ebook', this.ebookFormat]);
-
- // Convert images (svg -> png)
- this.convertImages = true;
-};
-util.inherits(Generator, BaseGenerator);
-
-Generator.prototype.prepareTemplates = function() {
- this.templates.page = this.book.plugins.template('ebook:page') || path.resolve(this.options.theme, 'templates/ebook/page.html');
- this.templates.summary = this.book.plugins.template('ebook:summary') || path.resolve(this.options.theme, 'templates/ebook/summary.html');
- this.templates.glossary = this.book.plugins.template('ebook:glossary') || path.resolve(this.options.theme, 'templates/ebook/glossary.html');
-
- return Q();
-};
-
-// Generate table of contents
-Generator.prototype.writeSummary = function() {
- var that = this;
-
- that.book.log.info.ln('write SUMMARY.html');
- return this._writeTemplate(this.templates.summary, {}, path.join(this.options.output, 'SUMMARY.html'));
-};
-
-// Return template for footer/header with inlined css
-Generator.prototype.getPDFTemplate = function(id) {
- var tpl = this.options.pdf[id+'Template'];
- var defaultTpl = path.resolve(this.options.theme, 'templates/ebook/'+id+'.html');
- var defaultCSS = path.resolve(this.options.theme, 'assets/ebook/pdf.css');
-
- // Default template from theme
- if (!tpl && fs.existsSync(defaultTpl)) {
- tpl = fs.readFileSync(defaultTpl, { encoding: 'utf-8' });
- }
-
- // Inline CSS using juice
- var stylesheets = [];
-
- // From theme
- if (fs.existsSync(defaultCSS)) {
- stylesheets.push(fs.readFileSync(defaultCSS, { encoding: 'utf-8' }));
- }
-
- // Custom PDF style
- if (this.styles.pdf) {
- stylesheets.push(fs.readFileSync(this.book.resolveOutput(this.styles.pdf), { encoding: 'utf-8' }));
- }
-
- tpl = juice(tpl, {
- extraCss: stylesheets.join('\n\n')
- });
-
- return tpl;
-};
-
-Generator.prototype.finish = function() {
- var that = this;
-
- return Q()
- .then(this.copyAssets)
- .then(this.copyCover)
- .then(this.writeGlossary)
- .then(this.writeSummary)
- .then(function() {
- if (!that.ebookFormat) return Q();
-
- if (!that.options.cover && fs.existsSync(path.join(that.options.output, 'cover.jpg'))) {
- that.options.cover = path.join(that.options.output, 'cover.jpg');
- }
-
- var d = Q.defer();
-
- var _options = {
- '--cover': that.options.cover,
- '--title': that.options.title,
- '--comments': that.options.description,
- '--isbn': that.options.isbn,
- '--authors': that.options.author,
- '--language': that.options.language,
- '--book-producer': 'GitBook',
- '--publisher': 'GitBook',
- '--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]',
- '--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]',
- '--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]',
- '--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]',
- '--no-chapters-in-toc': true,
- '--max-levels': '1',
- '--breadth-first': true
- };
-
- if (that.ebookFormat == 'pdf') {
- var pdfOptions = that.options.pdf;
-
- _.extend(_options, {
- '--chapter-mark': String(pdfOptions.chapterMark),
- '--page-breaks-before': String(pdfOptions.pageBreaksBefore),
- '--margin-left': String(pdfOptions.margin.left),
- '--margin-right': String(pdfOptions.margin.right),
- '--margin-top': String(pdfOptions.margin.top),
- '--margin-bottom': String(pdfOptions.margin.bottom),
- '--pdf-default-font-size': String(pdfOptions.fontSize),
- '--pdf-mono-font-size': String(pdfOptions.fontSize),
- '--paper-size': String(pdfOptions.paperSize),
- '--pdf-page-numbers': Boolean(pdfOptions.pageNumbers),
- '--pdf-header-template': that.getPDFTemplate('header'),
- '--pdf-footer-template': that.getPDFTemplate('footer'),
- '--pdf-sans-family': String(pdfOptions.fontFamily)
- });
- } else if (that.ebookFormat == 'epub') {
- _.extend(_options, {
- '--dont-split-on-page-breaks': true
- });
- }
-
- var command = [
- 'ebook-convert',
- path.join(that.options.output, 'SUMMARY.html'),
- path.join(that.options.output, 'index.'+that.ebookFormat),
- stringUtils.optionsToShellArgs(_options)
- ].join(' ');
-
- that.book.log.info('start conversion to', that.ebookFormat, '....');
-
- var child = exec(command, function (error, stdout) {
- if (error) {
- that.book.log.info.fail();
-
- if (error.code == 127) {
- error.message = 'Need to install ebook-convert from Calibre';
- } else {
- error.message = error.message + ' '+stdout;
- }
- return d.reject(error);
- }
-
- that.book.log.info.ok();
- d.resolve();
- });
-
- child.stdout.on('data', function (data) {
- that.book.log.debug(data);
- });
-
- child.stderr.on('data', function (data) {
- that.book.log.debug(data);
- });
-
- return d.promise;
- });
-};
-
-module.exports = Generator;
diff --git a/lib/generators/index.js b/lib/generators/index.js
deleted file mode 100644
index 068d0d994..000000000
--- a/lib/generators/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-var _ = require("lodash");
-var EbookGenerator = require("./ebook");
-
-module.exports = {
- json: require("./json"),
- website: require("./website"),
- ebook: EbookGenerator,
- pdf: _.partialRight(EbookGenerator, "pdf"),
- mobi: _.partialRight(EbookGenerator, "mobi"),
- epub: _.partialRight(EbookGenerator, "epub")
-};
diff --git a/lib/generators/json.js b/lib/generators/json.js
deleted file mode 100644
index 37ffa0b3f..000000000
--- a/lib/generators/json.js
+++ /dev/null
@@ -1,76 +0,0 @@
-var util = require('util');
-var path = require('path');
-var Q = require('q');
-var _ = require('lodash');
-
-var fs = require('../utils/fs');
-var BaseGenerator = require('../generator');
-var links = require('../utils/links');
-
-var Generator = function() {
- BaseGenerator.apply(this, arguments);
-};
-util.inherits(Generator, BaseGenerator);
-
-// Ignore some methods
-Generator.prototype.transferFile = function() { };
-
-// Convert an input file
-Generator.prototype.convertFile = function(input) {
- var that = this;
-
- return that.book.parsePage(input)
- .then(function(page) {
- var json = {
- progress: page.progress,
- sections: page.sections
- };
-
- var output = links.changeExtension(page.path, '.json');
- output = path.join(that.options.output, output);
-
- return fs.writeFile(
- output,
- JSON.stringify(json, null, 4)
- );
- });
-};
-
-// Finish generation
-Generator.prototype.finish = function() {
- return this.writeReadme();
-};
-
-// Write README.json
-Generator.prototype.writeReadme = function() {
- var that = this;
- var mainLang, langs, readme;
-
- return Q()
- .then(function() {
- langs = that.book.langs;
- mainLang = langs.length > 0? _.first(langs).lang : null;
-
- readme = links.changeExtension(that.book.readmeFile, '.json');
-
- // Read readme from main language
- return fs.readFile(
- mainLang? path.join(that.options.output, mainLang, readme) : path.join(that.options.output, readme)
- );
- })
- .then(function(content) {
- // Extend it with infos about the languages
- var json = JSON.parse(content);
- _.extend(json, {
- langs: langs
- });
-
- // Write it as README.json
- return fs.writeFile(
- path.join(that.options.output, 'README.json'),
- JSON.stringify(json, null, 4)
- );
- });
-};
-
-module.exports = Generator;
diff --git a/lib/generators/website.js b/lib/generators/website.js
deleted file mode 100644
index efb7c0fba..000000000
--- a/lib/generators/website.js
+++ /dev/null
@@ -1,268 +0,0 @@
-var util = require('util');
-var path = require('path');
-var Q = require('q');
-var _ = require('lodash');
-
-var nunjucks = require('nunjucks');
-var AutoEscapeExtension = require('nunjucks-autoescape')(nunjucks);
-var FilterExtension = require('nunjucks-filter')(nunjucks);
-
-var fs = require('../utils/fs');
-var BaseGenerator = require('../generator');
-var links = require('../utils/links');
-var i18n = require('../utils/i18n');
-
-var pkg = require('../../package.json');
-
-var Generator = function() {
- BaseGenerator.apply(this, arguments);
-
- // Revision
- this.revision = new Date();
-
- // Resources namespace
- this.namespace = 'website';
-
- // Style to integrates in the output
- this.styles = ['website'];
-
- // Convert images (svg -> png)
- this.convertImages = false;
-
- // Templates
- this.templates = {};
-};
-util.inherits(Generator, BaseGenerator);
-
-// Prepare the genertor
-Generator.prototype.prepare = function() {
- return BaseGenerator.prototype.prepare.apply(this)
- .then(this.prepareStyles)
- .then(this.prepareTemplates)
- .then(this.prepareTemplateEngine);
-};
-
-// Prepare all styles
-Generator.prototype.prepareStyles = function() {
- var that = this;
-
- this.styles = _.chain(this.styles)
- .map(function(style) {
- var stylePath = that.options.styles[style];
- var styleExists = (
- fs.existsSync(that.book.resolveOutput(stylePath)) ||
- fs.existsSync(that.book.resolve(stylePath))
- );
-
- if (stylePath && styleExists) {
- return [style, stylePath];
- }
- return null;
- })
- .compact()
- .object()
- .value();
-
- return Q();
-};
-
-// Prepare templates
-Generator.prototype.prepareTemplates = function() {
- this.templates.page = this.book.plugins.template('site:page') || path.resolve(this.options.theme, 'templates/website/page.html');
- this.templates.langs = this.book.plugins.template('site:langs') || path.resolve(this.options.theme, 'templates/website/langs.html');
- this.templates.glossary = this.book.plugins.template('site:glossary') || path.resolve(this.options.theme, 'templates/website/glossary.html');
-
- return Q();
-};
-
-// Prepare template engine
-Generator.prototype.prepareTemplateEngine = function() {
- var that = this;
-
- return Q()
- .then(function() {
- var language = that.book.config.normalizeLanguage();
-
- if (!i18n.hasLocale(language)) {
- that.book.log.warn.ln('Language "'+language+'" is not available as a layout locales (en, '+i18n.getLocales().join(', ')+')');
- }
-
- var folders = _.chain(that.templates)
- .values()
- .map(path.dirname)
- .uniq()
- .value();
-
- that.env = new nunjucks.Environment(
- new nunjucks.FileSystemLoader(folders),
- {
- autoescape: true
- }
- );
-
- // Add filter
- that.env.addFilter('contentLink', that.book.contentLink.bind(that.book));
- that.env.addFilter('lvl', function(lvl) {
- return lvl.split('.').length;
- });
-
- // Add extension
- that.env.addExtension('AutoEscapeExtension', new AutoEscapeExtension(that.env));
- that.env.addExtension('FilterExtension', new FilterExtension(that.env));
- });
-};
-
-// Finis generation
-Generator.prototype.finish = function() {
- return this.copyAssets()
- .then(this.copyCover)
- .then(this.writeGlossary)
- .then(this.writeLangsIndex);
-};
-
-// Convert an input file
-Generator.prototype.convertFile = function(input) {
- var that = this;
-
- return that.book.parsePage(input, {
- convertImages: that.convertImages,
- interpolateTemplate: function(page) {
- return that.callHook('page:before', page);
- },
- interpolateContent: function(page) {
- return that.callHook('page', page);
- }
- })
- .then(function(page) {
- var relativeOutput = that.book.contentPath(page.path);
- var output = path.join(that.options.output, relativeOutput);
-
- var basePath = path.relative(path.dirname(output), that.options.output) || '.';
- if (process.platform === 'win32') basePath = basePath.replace(/\\/g, '/');
-
- that.book.log.debug.ln('write parsed file', page.path, 'to', relativeOutput);
-
- return that._writeTemplate(that.templates.page, {
- progress: page.progress,
-
- _input: page.path,
- content: page.sections,
-
- basePath: basePath,
- staticBase: links.join(basePath, 'gitbook')
- }, output);
- });
-};
-
-// Write the index for langs
-Generator.prototype.writeLangsIndex = function() {
- if (!this.book.langs.length) return Q();
-
- return this._writeTemplate(this.templates.langs, {
- langs: this.book.langs
- }, path.join(this.options.output, 'index.html'));
-};
-
-// Write glossary
-Generator.prototype.writeGlossary = function() {
- // No glossary
- if (this.book.glossary.length === 0) return Q();
-
- return this._writeTemplate(this.templates.glossary, {}, path.join(this.options.output, 'GLOSSARY.html'));
-};
-
-// Convert a page into a normalized data set
-Generator.prototype.normalizePage = function(page) {
- var that = this;
-
- var _callHook = function(name) {
- return that.callHook(name, page)
- .then(function(_page) {
- page = _page;
- return page;
- });
- };
-
- return Q()
- .then(function() {
- return _callHook('page');
- })
- .then(function() {
- return page;
- });
-};
-
-// Generate a template
-Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
- var that = this;
-
- interpolate = interpolate || _.identity;
- return Q()
- .then(function() {
- return that.env.render(
- tpl,
- _.extend({
- gitbook: {
- version: pkg.version
- },
-
- styles: that.styles,
-
- revision: that.revision,
-
- title: that.options.title,
- description: that.options.description,
- language: that.book.config.normalizeLanguage(),
- innerlanguage: that.book.isSubBook()? that.book.config.get('language') : null,
-
- glossary: that.book.glossary,
-
- summary: that.book.summary,
- allNavigation: that.book.navigation,
-
- plugins: {
- resources: that.book.plugins.resources(that.namespace)
- },
- pluginsConfig: JSON.stringify(that.options.pluginsConfig),
- htmlSnippet: _.partial(_.partialRight(that.book.plugins.html, that, options), that.namespace),
-
- options: that.options,
-
- basePath: '.',
- staticBase: path.join('.', 'gitbook'),
-
- '__': that.book.i18n.bind(that.book)
- }, options)
- );
- })
- .then(interpolate)
- .then(function(html) {
- return fs.writeFile(
- output,
- html
- );
- });
-};
-
-// Copy assets
-Generator.prototype.copyAssets = function() {
- var that = this;
-
- // Copy gitbook assets
- return fs.copy(
- path.join(that.options.theme, 'assets/'+this.namespace),
- path.join(that.options.output, 'gitbook')
- )
-
- // Copy plugins assets
- .then(function() {
- return Q.all(
- _.map(that.book.plugins.list, function(plugin) {
- var pluginAssets = path.join(that.options.output, 'gitbook/plugins/', plugin.name);
- return plugin.copyAssets(pluginAssets, that.namespace);
- })
- );
- });
-};
-
-module.exports = Generator;
diff --git a/lib/version.js b/lib/gitbook.js
similarity index 86%
rename from lib/version.js
rename to lib/gitbook.js
index f0ae187f7..cbdcaed9c 100644
--- a/lib/version.js
+++ b/lib/gitbook.js
@@ -4,7 +4,7 @@ var pkg = require('../package.json');
var VERSION = pkg.version;
var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
-// Test if current current gitbook version satisfies a condition
+// Verify that this gitbook version satisfies a requirement
// We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
function satisfies(condition) {
// Test with real version
@@ -14,6 +14,8 @@ function satisfies(condition) {
return semver.satisfies(VERSION_STABLE, condition);
}
+
module.exports = {
+ version: pkg.version,
satisfies: satisfies
};
diff --git a/lib/index.js b/lib/index.js
index a23ec3fba..13a572da3 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,215 +1,5 @@
-/* eslint no-console: 0 */
-
-var Q = require('q');
-var _ = require('lodash');
-var path = require('path');
-var tinylr = require('tiny-lr');
-var color = require('bash-color');
-
var Book = require('./book');
-var initBook = require('./init');
-var Server = require('./utils/server');
-var stringUtils = require('./utils/string');
-var watch = require('./utils/watch');
-var logger = require('./utils/logger');
-
-var LOG_OPTION = {
- name: 'log',
- description: 'Minimum log level to display',
- values: _.chain(logger.LEVELS).keys().map(stringUtils.toLowerCase).value(),
- defaults: 'info'
-};
-
-var FORMAT_OPTION = {
- name: 'format',
- description: 'Format to build to',
- values: ['website', 'json', 'ebook'],
- defaults: 'website'
-};
-
-// Export init to gitbook library
-Book.init = initBook;
module.exports = {
- Book: Book,
- LOG_LEVELS: logger.LEVELS,
-
- commands: _.flatten([
- {
- name: 'build [book] [output]',
- description: 'build a book',
- options: [
- FORMAT_OPTION,
- LOG_OPTION
- ],
- exec: function(args, kwargs) {
- var input = args[0] || process.cwd();
- var output = args[1] || path.join(input, '_book');
-
- var book = new Book(input, _.extend({}, {
- 'config': {
- 'output': output
- },
- 'logLevel': kwargs.log
- }));
-
- return book.parse()
- .then(function() {
- return book.generate(kwargs.format);
- })
- .then(function(){
- console.log('');
- console.log(color.green('Done, without error'));
- });
- }
- },
-
- _.map(['pdf', 'epub', 'mobi'], function(ebookType) {
- return {
- name: ebookType+' [book] [output]',
- description: 'build a book to '+ebookType,
- options: [
- LOG_OPTION
- ],
- exec: function(args, kwargs) {
- var input = args[0] || process.cwd();
- var output = args[1];
-
- var book = new Book(input, _.extend({}, {
- 'logLevel': kwargs.log
- }));
-
- return book.parse()
- .then(function() {
- return book.generateFile(output, {
- ebookFormat: ebookType
- });
- })
- .then(function(){
- console.log('');
- console.log(color.green('Done, without error'));
- });
- }
- };
- }),
-
- {
- name: 'serve [book]',
- description: 'Build then serve a gitbook from a directory',
- options: [
- {
- name: 'port',
- description: 'Port for server to listen on',
- defaults: 4000
- },
- {
- name: 'lrport',
- description: 'Port for livereload server to listen on',
- defaults: 35729
- },
- {
- name: 'watch',
- description: 'Enable/disable file watcher',
- defaults: true
- },
- FORMAT_OPTION,
- LOG_OPTION
- ],
- exec: function(args, kwargs) {
- var input = args[0] || process.cwd();
- var server = new Server();
-
- // Init livereload server
- var lrServer = tinylr({});
- var lrPath;
-
- var generate = function() {
- if (server.isRunning()) console.log('Stopping server');
-
- return server.stop()
- .then(function() {
- var book = new Book(input, _.extend({}, {
- 'config': {
- 'defaultsPlugins': ['livereload']
- },
- 'logLevel': kwargs.log
- }));
-
- return book.parse()
- .then(function() {
- return book.generate(kwargs.format);
- })
- .thenResolve(book);
- })
- .then(function(book) {
- console.log();
- console.log('Starting server ...');
- return server.start(book.options.output, kwargs.port)
- .then(function() {
- console.log('Serving book on http://localhost:'+kwargs.port);
-
- if (lrPath) {
- // trigger livereload
- lrServer.changed({
- body: {
- files: [lrPath]
- }
- });
- }
-
- if (!kwargs.watch) return;
-
- return watch(book.root)
- .then(function(filepath) {
- // set livereload path
- lrPath = filepath;
- console.log('Restart after change in file', filepath);
- console.log('');
- return generate();
- });
- });
- });
- };
-
- return Q.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
- .then(function() {
- console.log('Live reload server started on port:', kwargs.lrport);
- console.log('Press CTRL+C to quit ...');
- console.log('');
- return generate();
- });
- }
- },
-
- {
- name: 'install [book]',
- description: 'install plugins dependencies',
- exec: function(args) {
- var input = args[0] || process.cwd();
-
- var book = new Book(input);
-
- return book.config.load()
- .then(function() {
- return book.plugins.install();
- })
- .then(function(){
- console.log('');
- console.log(color.green('Done, without error'));
- });
- }
- },
-
- {
- name: 'init [directory]',
- description: 'create files and folders based on contents of SUMMARY.md',
- exec: function(args) {
- return initBook(args[0] || process.cwd())
- .then(function(){
- console.log('');
- console.log(color.green('Done, without error'));
- });
- }
- }
- ])
+ Book: Book
};
diff --git a/lib/init.js b/lib/init.js
deleted file mode 100644
index 2fc8016c9..000000000
--- a/lib/init.js
+++ /dev/null
@@ -1,83 +0,0 @@
-var _ = require('lodash');
-var Q = require('q');
-var path = require('path');
-
-var Book = require('./book');
-var fs = require('./utils/fs');
-
-// Initialize folder structure for a book
-// Read SUMMARY to created the right chapter
-function initBook(root, opts) {
- var book = new Book(root, opts);
- var extensionToUse = '.md';
-
- var chaptersPaths = function(chapters) {
- return _.reduce(chapters || [], function(accu, chapter) {
- var o = {
- title: chapter.title
- };
- if (chapter.path) o.path = chapter.path;
-
- return accu.concat(
- [o].concat(chaptersPaths(chapter.articles))
- );
- }, []);
- };
-
- book.log.info.ln('init book at', root);
- return fs.mkdirp(root)
- .then(function() {
- book.log.info.ln('detect structure from SUMMARY (if it exists)');
- return book.parseSummary();
- })
- .fail(function() {
- return Q();
- })
- .then(function() {
- var summary = book.summaryFile || 'SUMMARY.md';
- var chapters = book.summary.chapters || [];
- extensionToUse = path.extname(summary);
-
- if (chapters.length === 0) {
- chapters = [
- {
- title: 'Summary',
- path: 'SUMMARY'+extensionToUse
- },
- {
- title: 'Introduction',
- path: 'README'+extensionToUse
- }
- ];
- }
-
- return Q(chaptersPaths(chapters));
- })
- .then(function(chapters) {
- // Create files that don't exist
- return Q.all(_.map(chapters, function(chapter) {
- if (!chapter.path) return Q();
- var absolutePath = path.resolve(book.root, chapter.path);
-
- return fs.exists(absolutePath)
- .then(function(exists) {
- if(exists) {
- book.log.info.ln('found', chapter.path);
- return;
- } else {
- book.log.info.ln('create', chapter.path);
- }
-
- return fs.mkdirp(path.dirname(absolutePath))
- .then(function() {
- return fs.writeFile(absolutePath, '# '+chapter.title+'\n');
- });
- });
- }));
- })
- .then(function() {
- book.log.info.ln('initialization is finished');
- });
-}
-
-module.exports = initBook;
diff --git a/lib/logger.js b/lib/logger.js
new file mode 100644
index 000000000..74281802c
--- /dev/null
+++ b/lib/logger.js
@@ -0,0 +1,123 @@
+var _ = require('lodash');
+var util = require('util');
+var color = require('bash-color');
+
+var LEVELS = {
+ DEBUG: 0,
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3,
+ DISABLED: 10
+};
+
+var COLORS = {
+ DEBUG: color.purple,
+ INFO: color.cyan,
+ WARN: color.yellow,
+ ERROR: color.red
+};
+
+function Logger(write, logLevel) {
+ if (!(this instanceof Logger)) return new Logger(write, logLevel);
+
+ this._write = write;
+ this.lastChar = '\n';
+
+ // Define log level
+ this.setLevel(logLevel);
+
+ _.bindAll(this);
+
+ // Create easy-to-use method like "logger.debug.ln('....')"
+ _.each(_.omit(LEVELS, 'DISABLED'), function(level, levelKey) {
+ levelKey = levelKey.toLowerCase();
+
+ this[levelKey] = _.partial(this.log, level);
+ this[levelKey].ln = _.partial(this.logLn, level);
+ this[levelKey].ok = _.partial(this.ok, level);
+ this[levelKey].fail = _.partial(this.fail, level);
+ this[levelKey].promise = _.partial(this.promise, level);
+ }, this);
+}
+
+// Change minimum level
+Logger.prototype.setLevel = function(logLevel) {
+ if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
+ this.logLevel = logLevel;
+};
+
+// Print a simple string
+Logger.prototype.write = function(msg) {
+ msg = msg.toString();
+ this.lastChar = _.last(msg);
+ return this._write(msg);
+};
+
+// Format a string using the first argument as a printf-like format.
+Logger.prototype.format = function() {
+ return util.format.apply(util, arguments);
+};
+
+// Print a line
+Logger.prototype.writeLn = function(msg) {
+ return this.write((msg || '')+'\n');
+};
+
+// Log/Print a message if level is allowed
+Logger.prototype.log = function(level) {
+ if (level < this.logLevel) return;
+
+ var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
+ var args = Array.prototype.slice.apply(arguments, [1]);
+ var msg = this.format.apply(this, args);
+
+ if (this.lastChar == '\n') {
+ msg = COLORS[levelKey](levelKey.toLowerCase()+':')+' '+msg;
+ }
+
+ return this.write(msg);
+};
+
+// Log/Print a line if level is allowed
+Logger.prototype.logLn = function() {
+ if (this.lastChar != '\n') this.write('\n');
+
+ var args = Array.prototype.slice.apply(arguments);
+ args.push('\n');
+ return this.log.apply(this, args);
+};
+
+// Log a confirmation [OK]
+Logger.prototype.ok = function(level) {
+ var args = Array.prototype.slice.apply(arguments, [1]);
+ var msg = this.format.apply(this, args);
+ if (arguments.length > 1) {
+ this.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> ')));
+ } else {
+ this.log(level, color.green('OK'), '\n');
+ }
+};
+
+// Log a "FAIL"
+Logger.prototype.fail = function(level) {
+ return this.log(level, color.red('ERROR') + '\n');
+};
+
+// Log state of a promise
+Logger.prototype.promise = function(level, p) {
+ var that = this;
+
+ return p.
+ then(function(st) {
+ that.ok(level);
+ return st;
+ }, function(err) {
+ that.fail(level);
+ throw err;
+ });
+};
+
+Logger.LEVELS = LEVELS;
+Logger.COLORS = COLORS;
+
+module.exports = Logger;
diff --git a/lib/page.js b/lib/page.js
new file mode 100644
index 000000000..a6c2caf98
--- /dev/null
+++ b/lib/page.js
@@ -0,0 +1,10 @@
+
+
+function Page() {
+ if (!(this instanceof Page)) return new Page();
+
+}
+
+
+
+module.exports = Page;
diff --git a/lib/plugin.js b/lib/plugin.js
deleted file mode 100644
index b7e826017..000000000
--- a/lib/plugin.js
+++ /dev/null
@@ -1,241 +0,0 @@
-var _ = require('lodash');
-var Q = require('q');
-var path = require('path');
-var url = require('url');
-var fs = require('./utils/fs');
-var resolve = require('resolve');
-var mergeDefaults = require('merge-defaults');
-var jsonschema = require('jsonschema');
-var jsonSchemaDefaults = require('json-schema-defaults');
-
-var version = require('./version');
-
-var PLUGIN_PREFIX = 'gitbook-plugin-';
-
-// Return an absolute name for the plugin (the one on NPM)
-function absoluteName(name) {
- if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
- return [PLUGIN_PREFIX, name].join('');
-}
-
-
-var Plugin = function(book, name) {
- this.book = book;
- this.name = absoluteName(name);
- this.packageInfos = {};
- this.infos = {};
-
- // Bind methods
- _.bindAll(this);
-
- _.each([
- absoluteName(name),
- name
- ], function(_name) {
- // Load from the book
- if (this.load(_name, book.root)) return false;
-
- // Load from default plugins
- if (this.load(_name, __dirname)) return false;
- }, this);
-};
-
-// Type of plugins resources
-Plugin.RESOURCES = ['js', 'css'];
-Plugin.HOOKS = [
- 'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
-];
-
-// Return the reduce name for the plugin
-// "gitbook-plugin-test" -> "test"
-// Return a relative name for the plugin (the one on GitBook)
-Plugin.prototype.reducedName = function() {
- return this.name.replace(PLUGIN_PREFIX, '');
-};
-
-// Load from a name
-Plugin.prototype.load = function(name, baseDir) {
- try {
- var res = resolve.sync(name+'/package.json', { basedir: baseDir });
-
- this.baseDir = path.dirname(res);
- this.packageInfos = require(res);
- this.infos = require(resolve.sync(name, { basedir: baseDir }));
- this.name = this.packageInfos.name;
-
- return true;
- } catch (e) {
- this.packageInfos = {};
- this.infos = {};
- return false;
- }
-};
-
-Plugin.prototype.normalizeResource = function(resource) {
- // Parse the resource path
- var parsed = url.parse(resource);
-
- // This is a remote resource
- // so we will simply link to using it's URL
- if (parsed.protocol) {
- return {
- 'url': resource
- };
- }
-
- // This will be copied over from disk
- // and shipped with the book's build
- return { 'path': this.name+'/'+resource };
-};
-
-// Return resources
-Plugin.prototype._getResources = function(base) {
- base = base;
- var book = this.infos[base];
-
- // Compatibility with version 1.x.x
- if (base == 'website') book = book || this.infos.book;
-
- // Nothing specified, fallback to default
- if (!book) {
- return Q({});
- }
-
- // Dynamic function
- if(typeof book === 'function') {
- // Call giving it the context of our book
- return Q().then(book.bind(this.book));
- }
-
- // Plain data object
- return Q(_.cloneDeep(book));
-};
-
-// Normalize resources and return them
-Plugin.prototype.getResources = function(base) {
- var that = this;
-
- return this._getResources(base)
- .then(function(resources) {
-
- _.each(Plugin.RESOURCES, function(resourceType) {
- resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
- });
-
- return resources;
- });
-};
-
-// Normalize filters and return them
-Plugin.prototype.getFilters = function() {
- return this.infos.filters || {};
-};
-
-// Normalize blocks and return them
-Plugin.prototype.getBlocks = function() {
- return this.infos.blocks || {};
-};
-
-// Test if it's a valid plugin
-Plugin.prototype.isValid = function() {
- var that = this;
- var isValid = (
- this.packageInfos &&
- this.packageInfos.name &&
- this.packageInfos.engines &&
- this.packageInfos.engines.gitbook &&
- version.satisfies(this.packageInfos.engines.gitbook)
- );
-
- // Valid hooks
- _.each(this.infos.hooks, function(hook, hookName) {
- if (_.contains(Plugin.HOOKS, hookName)) return;
- that.book.log.warn.ln('Hook "'+hookName+'"" used by plugin "'+that.packageInfos.name+'" has been removed or is deprecated');
- });
-
- return isValid;
-};
-
-// Normalize, validate configuration for this plugin using its schema
-// Throw an error when shcema is not respected
-Plugin.prototype.validateConfig = function(config) {
- var that = this;
-
- return Q()
- .then(function() {
- var schema = that.packageInfos.gitbook || {};
- if (!schema) return config;
-
- // Normalize schema
- schema.id = '/pluginsConfig.'+that.reducedName();
- schema.type = 'object';
-
- // Validate and throw if invalid
- var v = new jsonschema.Validator();
- var result = v.validate(config, schema, {
- propertyName: 'pluginsConfig.'+that.reducedName()
- });
-
- // Throw error
- if (result.errors.length > 0) {
- throw new Error('Configuration Error: '+result.errors[0].stack);
- }
-
- // Insert default values
- var defaults = jsonSchemaDefaults(schema);
- return mergeDefaults(config, defaults);
- });
-};
-
-// Resolve file path
-Plugin.prototype.resolveFile = function(filename) {
- return path.resolve(this.baseDir, filename);
-};
-
-// Resolve file path
-Plugin.prototype.callHook = function(name, data) {
- // Our book will be the context to apply
- var context = this.book;
-
- var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
- data = data || {};
-
- if (!hookFunc) return Q(data);
-
- this.book.log.debug.ln('call hook', name);
- if (!_.contains(Plugin.HOOKS, name)) this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions');
-
- return Q()
- .then(function() {
- return hookFunc.apply(context, [data]);
- });
-};
-
-// Copy plugin assets fodler
-Plugin.prototype.copyAssets = function(out, base) {
- var that = this;
-
- return this.getResources(base)
- .get('assets')
- .then(function(assets) {
- // Assets are undefined
- if(!assets) return false;
-
- return fs.copy(
- that.resolveFile(assets),
- out
- ).then(_.constant(true));
- }, _.constant(false));
-};
-
-// Get config from book
-Plugin.prototype.getConfig = function() {
- return this.book.config.get('pluginsConfig.'+this.reducedName(), {});
-};
-
-// Set configuration for this plugin
-Plugin.prototype.setConfig = function(values) {
- return this.book.config.set('pluginsConfig.'+this.reducedName(), values);
-};
-
-module.exports = Plugin;
diff --git a/lib/pluginslist.js b/lib/pluginslist.js
deleted file mode 100644
index 290cd354a..000000000
--- a/lib/pluginslist.js
+++ /dev/null
@@ -1,230 +0,0 @@
-var _ = require('lodash');
-var Q = require('q');
-var npmi = require('npmi');
-var npm = require('npm');
-var semver = require('semver');
-
-var Plugin = require('./plugin');
-var version = require('./version');
-
-var initNPM = _.memoize(function() {
- return Q.nfcall(npm.load, { silent: true, loglevel: 'silent' });
-});
-
-
-var PluginsList = function(book, plugins) {
- this.book = book;
- this.log = this.book.log;
-
- // List of Plugin objects
- this.list = [];
-
- // List of names of failed plugins
- this.failed = [];
-
- // Namespaces
- this.namespaces = _.chain(['website', 'ebook'])
- .map(function(namespace) {
- return [
- namespace,
- {
- html: {},
- resources: _.chain(Plugin.RESOURCES)
- .map(function(type) {
- return [type, []];
- })
- .object()
- .value()
- }
- ];
- })
- .object()
- .value();
-
- // Bind methods
- _.bindAll(this);
-
- if (plugins) this.load(plugins);
-};
-
-// return count of plugins
-PluginsList.prototype.count = function() {
- return this.list.length;
-};
-
-// Add and load a plugin
-PluginsList.prototype.load = function(plugin) {
- var that = this;
-
- if (_.isArray(plugin)) {
- return _.reduce(plugin, function(prev, p) {
- return prev.then(function() {
- return that.load(p);
- });
- }, Q());
- }
- if (_.isObject(plugin) && !(plugin instanceof Plugin)) plugin = plugin.name;
- if (_.isString(plugin)) plugin = new Plugin(this.book, plugin);
-
- that.log.info('load plugin', plugin.name, '....');
- if (!plugin.isValid()) {
- that.log.info.fail();
- that.failed.push(plugin.name);
- return Q();
- } else {
- that.log.info.ok();
-
- // Push in the list
- that.list.push(plugin);
- }
-
- return Q()
-
- // Validate and normalize configuration
- .then(function() {
- var config = plugin.getConfig();
- return plugin.validateConfig(config);
- })
- .then(function(config) {
- // Update configuration
- plugin.setConfig(config);
-
- // Extract filters
- that.book.template.addFilters(plugin.getFilters());
-
- // Extract blocks
- that.book.template.addBlocks(plugin.getBlocks());
-
- return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) {
- return prev.then(function() {
- return plugin.getResources(namespaceName)
- .then(function(plResources) {
- var namespace = that.namespaces[namespaceName];
-
- // Extract js and css
- _.each(Plugin.RESOURCES, function(resourceType) {
- namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []);
- });
-
- // Map of html resources by name added by each plugin
- _.each(plResources.html || {}, function(value, tag) {
- // Turn into function if not one already
- if (!_.isFunction(value)) value = _.constant(value);
-
- namespace.html[tag] = namespace.html[tag] || [];
- namespace.html[tag].push(value);
- });
- });
- });
- }, Q());
- });
-};
-
-// Call a hook
-PluginsList.prototype.hook = function(name, data) {
- return _.reduce(this.list, function(prev, plugin) {
- return prev.then(function(ret) {
- return plugin.callHook(name, ret);
- });
- }, Q(data));
-};
-
-// Return a template from a plugin
-PluginsList.prototype.template = function(name) {
- var withTpl = _.find(this.list, function(plugin) {
- return (
- plugin.infos.templates &&
- plugin.infos.templates[name]
- );
- });
-
- if (!withTpl) return null;
- return withTpl.resolveFile(withTpl.infos.templates[name]);
-};
-
-// Return an html snippet
-PluginsList.prototype.html = function(namespace, tag, context, options) {
- var htmlSnippets = this.namespaces[namespace].html[tag];
- return _.map(htmlSnippets || [], function(code) {
- return code.call(context, options);
- }).join('\n');
-};
-
-// Return a resources map for a namespace
-PluginsList.prototype.resources = function(namespace) {
- return this.namespaces[namespace].resources;
-};
-
-// Install plugins from a book
-PluginsList.prototype.install = function() {
- var that = this;
-
- // Remove defaults (no need to install)
- var plugins = _.reject(that.book.options.plugins, {
- isDefault: true
- });
-
- // Install plugins one by one
- that.book.log.info.ln(plugins.length+' plugins to install');
- return _.reduce(plugins, function(prev, plugin) {
- return prev.then(function() {
- var fullname = 'gitbook-plugin-'+plugin.name;
-
- return Q()
-
- // Resolve version if needed
- .then(function() {
- if (plugin.version) return plugin.version;
-
- that.book.log.info.ln('No version specified, resolve plugin', plugin.name);
- return initNPM()
- .then(function() {
- return Q.nfcall(npm.commands.view, [fullname+'@*', 'engines'], true);
- })
- .then(function(versions) {
- return _.chain(versions)
- .pairs()
- .map(function(v) {
- return {
- version: v[0],
- gitbook: (v[1].engines || {}).gitbook
- };
- })
- .filter(function(v) {
- return v.gitbook && version.satisfies(v.gitbook);
- })
- .sort(function(v1, v2) {
- return semver.lt(v1.version, v2.version)? 1 : -1;
- })
- .pluck('version')
- .first()
- .value();
- });
- })
-
- // Install the plugin with the resolved version
- .then(function(version) {
- if (!version) {
- throw 'Found no satisfactory version for plugin '+plugin.name;
- }
-
- that.book.log.info.ln('install plugin', plugin.name, 'from npm ('+fullname+') with version', version);
- return Q.nfcall(npmi, {
- 'name': fullname,
- 'version': version,
- 'path': that.book.root,
- 'npmLoad': {
- 'loglevel': 'silent',
- 'loaded': true,
- 'prefix': that.book.root
- }
- });
- })
- .then(function() {
- that.book.log.info.ok('plugin', plugin.name, 'installed with success');
- });
- });
- }, Q());
-};
-
-module.exports = PluginsList;
diff --git a/lib/template.js b/lib/template.js
deleted file mode 100644
index dac1201ed..000000000
--- a/lib/template.js
+++ /dev/null
@@ -1,466 +0,0 @@
-var _ = require('lodash');
-var Q = require('q');
-var path = require('path');
-var nunjucks = require('nunjucks');
-var parsers = require('gitbook-parsers');
-var escapeStringRegexp = require('escape-string-regexp');
-
-var batch = require('./utils/batch');
-var pkg = require('../package.json');
-var defaultBlocks = require('./blocks');
-var BookLoader = require('./conrefs_loader');
-
-// Normalize result from a block
-function normBlockResult(blk) {
- if (_.isString(blk)) blk = { body: blk };
- return blk;
-}
-
-
-var TemplateEngine = function(book) {
- var that = this;
-
- this.book = book;
- this.log = this.book.log;
-
- // Template loader
- this.loader = new BookLoader(book, {
- // Replace shortcuts in imported files
- interpolate: function(filepath, source) {
- var parser = parsers.get(path.extname(filepath));
- var type = parser? parser.name : null;
-
- return that.applyShortcuts(type, source);
- }
- });
-
- // Nunjucks env
- this.env = new nunjucks.Environment(
- this.loader,
- {
- // Escaping is done after by the markdown parser
- autoescape: false,
-
- // Tags
- tags: {
- blockStart: '{%',
- blockEnd: '%}',
- variableStart: '{{',
- variableEnd: '}}',
- commentStart: '{###',
- commentEnd: '###}'
- }
- }
- );
-
- // List of tags shortcuts
- this.shortcuts = [];
-
- // Map of blocks bodies (that requires post-processing)
- this.blockBodies = {};
-
- // Map of added blocks
- this.blocks = {};
-
- // Bind methods
- _.bindAll(this);
-
- // Add default blocks
- this.addBlocks(defaultBlocks);
-};
-
-// Process the result of block in a context
-TemplateEngine.prototype.processBlock = function(blk) {
- blk = _.defaults(blk, {
- parse: false,
- post: undefined
- });
- blk.id = _.uniqueId('blk');
-
- var toAdd = (!blk.parse) || (blk.post !== undefined);
-
- // Add to global map
- if (toAdd) this.blockBodies[blk.id] = blk;
-
- // Parsable block, just return it
- if (blk.parse) {
- return blk.body;
- }
-
- // Return it as a position marker
- return '@%@'+blk.id+'@%@';
-};
-
-// Replace position markers of blocks by body after processing
-// This is done to avoid that markdown/asciidoc processer parse the block content
-TemplateEngine.prototype.replaceBlocks = function(content) {
- var that = this;
-
- return content.replace(/\@\%\@([\s\S]+?)\@\%\@/g, function(match, key) {
- var blk = that.blockBodies[key];
- if (!blk) return match;
-
- var body = blk.body;
-
- return body;
- });
-};
-
-// Bind a function to a context
-TemplateEngine.prototype.bindContext = function(func) {
- var that = this;
-
- return function() {
- var ctx = {
- ctx: this.ctx,
- book: that.book,
- generator: that.book.options.generator
- };
-
- return func.apply(ctx, arguments);
- };
-};
-
-// Add filter
-TemplateEngine.prototype.addFilter = function(filterName, func) {
- try {
- this.env.getFilter(filterName);
- this.log.warn.ln('conflict in filters, \''+filterName+'\' is already set');
- return false;
- } catch(e) {
- // Filter doesn't exist
- }
-
- this.log.debug.ln('add filter \''+filterName+'\'');
- this.env.addFilter(filterName, this.bindContext(function() {
- var ctx = this;
- var args = Array.prototype.slice.apply(arguments);
- var callback = _.last(args);
-
- Q()
- .then(function() {
- return func.apply(ctx, args.slice(0, -1));
- })
- .nodeify(callback);
- }), true);
- return true;
-};
-
-// Add multiple filters
-TemplateEngine.prototype.addFilters = function(filters) {
- _.each(filters, function(filter, name) {
- this.addFilter(name, filter);
- }, this);
-};
-
-// Return nunjucks extension name of a block
-TemplateEngine.prototype.blockExtName = function(name) {
- return 'Block'+name+'Extension';
-};
-
-// Test if a block is defined
-TemplateEngine.prototype.hasBlock = function(name) {
- return this.env.hasExtension(this.blockExtName(name));
-};
-
-// Remove a block
-TemplateEngine.prototype.removeBlock = function(name) {
- if (!this.hasBlock(name)) return;
-
- // Remove nunjucks extension
- this.env.removeExtension(this.blockExtName(name));
-
- // Cleanup shortcuts
- this.shortcuts = _.reject(this.shortcuts, {
- block: name
- });
-};
-
-// Add a block
-TemplateEngine.prototype.addBlock = function(name, block) {
- var that = this, Ext, extName;
-
- if (_.isFunction(block)) block = { process: block };
-
- block = _.defaults(block || {}, {
- shortcuts: [],
- end: 'end'+name,
- process: _.identity,
- blocks: []
- });
-
- extName = this.blockExtName(name);
-
- if (this.hasBlock(name) && !defaultBlocks[name]) {
- this.log.warn.ln('conflict in blocks, \''+name+'\' is already defined');
- }
-
- // Cleanup previous block
- this.removeBlock(name);
-
- this.log.debug.ln('add block \''+name+'\'');
- this.blocks[name] = block;
-
- Ext = function () {
- this.tags = [name];
-
- this.parse = function(parser, nodes) {
- var body = null;
- var lastBlockName = null;
- var lastBlockArgs = null;
- var allBlocks = block.blocks.concat([block.end]);
- var subbodies = {};
-
- var tok = parser.nextToken();
- var args = parser.parseSignature(null, true);
- parser.advanceAfterBlockEnd(tok.value);
-
- while (1) {
- // Read body
- var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
-
- // Handle body with previous block name and args
- if (lastBlockName) {
- subbodies[lastBlockName] = subbodies[lastBlockName] || [];
- subbodies[lastBlockName].push({
- body: currentBody,
- args: lastBlockArgs
- });
- } else {
- body = currentBody;
- }
-
- // Read new block
- lastBlockName = parser.peekToken().value;
- if (lastBlockName == block.end) {
- break;
- }
-
- // Parse signature and move to the end of the block
- lastBlockArgs = parser.parseSignature(null, true);
- parser.advanceAfterBlockEnd(lastBlockName);
- }
- parser.advanceAfterBlockEnd();
-
- var bodies = [body];
- _.each(block.blocks, function(blockName) {
- subbodies[blockName] = subbodies[blockName] || [];
- if (subbodies[blockName].length === 0) {
- subbodies[blockName].push({
- args: new nodes.NodeList(),
- body: new nodes.NodeList()
- });
- }
-
- bodies.push(subbodies[blockName][0].body);
- });
-
- return new nodes.CallExtensionAsync(this, 'run', args, bodies);
- };
-
- this.run = function(context) {
- var args = Array.prototype.slice.call(arguments, 1);
- var callback = args.pop();
-
- // Extract blocks
- var blocks = args
- .concat([])
- .slice(-block.blocks.length);
-
- // Eliminate blocks from list
- if (block.blocks.length > 0) args = args.slice(0, -block.blocks.length);
-
- // Extract main body and kwargs
- var body = args.pop();
- var kwargs = _.isObject(_.last(args))? args.pop() : {};
-
- // Extract blocks body
- var _blocks = _.map(block.blocks, function(blockName, i){
- return {
- name: blockName,
- body: blocks[i]()
- };
- });
-
- Q()
- .then(function() {
- return that.applyBlock(name, {
- body: body(),
- args: args,
- kwargs: kwargs,
- blocks: _blocks
- }, context);
- })
-
- // process the block returned
- .then(that.processBlock)
- .nodeify(callback);
- };
- };
-
- // Add the Extension
- this.env.addExtension(extName, new Ext());
-
- // Add shortcuts
- if (!_.isArray(block.shortcuts)) block.shortcuts = [block.shortcuts];
- _.each(block.shortcuts, function(shortcut) {
- this.log.debug.ln('add template shortcut from \''+shortcut.start+'\' to block \''+name+'\' for parsers ', shortcut.parsers);
- this.shortcuts.push({
- block: name,
- parsers: shortcut.parsers,
- start: shortcut.start,
- end: shortcut.end,
- tag: {
- start: name,
- end: block.end
- }
- });
- }, this);
-};
-
-// Add multiple blocks
-TemplateEngine.prototype.addBlocks = function(blocks) {
- _.each(blocks, function(block, name) {
- this.addBlock(name, block);
- }, this);
-};
-
-// Apply a block to some content
-// This method result depends on the type of block (async or sync)
-TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
- var func, block, r;
-
- block = this.blocks[name];
- if (!block) throw new Error('Block not found \''+name+'\'');
- if (_.isString(blk)) {
- blk = {
- body: blk
- };
- }
-
- blk = _.defaults(blk, {
- args: [],
- kwargs: {},
- blocks: []
- });
-
- // Bind and call block processor
- func = this.bindContext(block.process);
- r = func.call(ctx || {}, blk);
-
- if (Q.isPromise(r)) return r.then(normBlockResult);
- else return normBlockResult(r);
-};
-
-// Apply a shortcut to a string
-TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) {
- if (!_.contains(shortcut.parsers, parser)) return content;
- var regex = new RegExp(
- escapeStringRegexp(shortcut.start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(shortcut.end),
- 'g'
- );
- return content.replace(regex, function(all, match) {
- return '{% '+shortcut.tag.start+' %}'+ match + '{% '+shortcut.tag.end+' %}';
- });
-};
-
-// Apply all shortcuts to some template string
-TemplateEngine.prototype.applyShortcuts = function(type, content) {
- return _.reduce(this.shortcuts, _.partial(this._applyShortcut.bind(this), type), content);
-};
-
-// Render a string from the book
-TemplateEngine.prototype.renderString = function(content, context, options) {
- context = _.extend({}, context, {
- // Variables from book.json
- book: this.book.options.variables,
-
- // Complete book.json
- config: this.book.options,
-
- // infos about gitbook
- gitbook: {
- version: pkg.version,
- generator: this.book.options.generator
- }
- });
- options = _.defaults(options || {}, {
- path: null,
- type: null
- });
- if (options.path) options.path = this.book.resolve(options.path);
- if (!options.type && options.path) {
- var parser = parsers.get(path.extname(options.path));
- options.type = parser? parser.name : null;
- }
-
- // Replace shortcuts
- content = this.applyShortcuts(options.type, content);
-
- return Q.nfcall(this.env.renderString.bind(this.env), content, context, options)
- .fail(function(err) {
- if (_.isString(err)) err = new Error(err);
- err.message = err.message.replace(/^Error: /, '');
-
- throw err;
- });
-};
-
-// Render a file from the book
-TemplateEngine.prototype.renderFile = function(filename) {
- var that = this;
-
- return that.book.readFile(filename)
- .then(function(content) {
- return that.renderString(content, {}, {
- path: filename
- });
- });
-};
-
-// Render a page from the book
-TemplateEngine.prototype.renderPage = function(page) {
- var that = this;
-
- return that.book.statFile(page.path)
- .then(function(stat) {
- var context = {
- // infos about the file
- file: {
- path: page.path,
- mtime: stat.mtime
- }
- };
-
- return that.renderString(page.content, context, {
- path: page.path,
- type: page.type
- });
- });
-};
-
-// Post process content
-TemplateEngine.prototype.postProcess = function(content) {
- var that = this;
-
- return Q(content)
- .then(that.replaceBlocks)
- .then(function(_content) {
- return batch.execEach(that.blockBodies, {
- max: 20,
- fn: function(blk, blkId) {
- return Q()
- .then(function() {
- if (!blk.post) return Q();
- return blk.post();
- })
- .then(function() {
- delete that.blockBodies[blkId];
- });
- }
- })
- .thenResolve(_content);
- });
-};
-
-module.exports = TemplateEngine;
diff --git a/lib/utils/batch.js b/lib/utils/batch.js
deleted file mode 100644
index 90697664f..000000000
--- a/lib/utils/batch.js
+++ /dev/null
@@ -1,52 +0,0 @@
-var Q = require("q");
-var _ = require("lodash");
-
-// Execute a method for all element
-function execEach(items, options) {
- if (_.size(items) === 0) return Q();
- var concurrents = 0, d = Q.defer(), pending = [];
-
- options = _.defaults(options || {}, {
- max: 100,
- fn: function() {}
- });
-
-
- function startItem(item, i) {
- if (concurrents >= options.max) {
- pending.push([item, i]);
- return;
- }
-
- concurrents++;
- Q()
- .then(function() {
- return options.fn(item, i);
- })
- .then(function() {
- concurrents--;
-
- // Next pending
- var next = pending.shift();
-
- if (concurrents === 0 && !next) {
- d.resolve();
- } else if (next) {
- startItem.apply(null, next);
- }
- })
- .fail(function(err) {
- pending = [];
- d.reject(err);
- });
- }
-
- _.each(items, startItem);
-
- return d.promise;
-}
-
-module.exports = {
- execEach: execEach
-};
-
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
deleted file mode 100644
index b82701f98..000000000
--- a/lib/utils/fs.js
+++ /dev/null
@@ -1,193 +0,0 @@
-var _ = require('lodash');
-var Q = require('q');
-var tmp = require('tmp');
-var path = require('path');
-var fs = require('graceful-fs');
-var fsExtra = require('fs-extra');
-var Ignore = require('fstream-ignore');
-
-var fsUtils = {
- tmp: {
- file: function(opt) {
- return Q.nfcall(tmp.file.bind(tmp), opt).get(0);
- },
- dir: function() {
- return Q.nfcall(tmp.dir.bind(tmp)).get(0);
- }
- },
- list: listFiles,
- stat: Q.denodeify(fs.stat),
- readdir: Q.denodeify(fs.readdir),
- readFile: Q.denodeify(fs.readFile),
- writeFile: writeFile,
- writeStream: writeStream,
- mkdirp: Q.denodeify(fsExtra.mkdirp),
- copy: Q.denodeify(fsExtra.copy),
- remove: Q.denodeify(fsExtra.remove),
- symlink: Q.denodeify(fsExtra.symlink),
- exists: function(path) {
- var d = Q.defer();
- fs.exists(path, d.resolve);
- return d.promise;
- },
- findFile: findFile,
- existsSync: fs.existsSync.bind(fs),
- readFileSync: fs.readFileSync.bind(fs),
- clean: cleanFolder,
- getUniqueFilename: getUniqueFilename
-};
-
-// Write a file
-function writeFile(filename, data, options) {
- var d = Q.defer();
-
- try {
- fs.writeFileSync(filename, data, options);
- } catch(err) {
- d.reject(err);
- }
- d.resolve();
-
-
- return d.promise;
-}
-
-// Write a stream to a file
-function writeStream(filename, st) {
- var d = Q.defer();
-
- var wstream = fs.createWriteStream(filename);
-
- wstream.on('finish', function () {
- d.resolve();
- });
- wstream.on('error', function (err) {
- d.reject(err);
- });
-
- st.on('error', function(err) {
- d.reject(err);
- });
-
- st.pipe(wstream);
-
- return d.promise;
-}
-
-// Find a filename available
-function getUniqueFilename(base, filename) {
- if (!filename) {
- filename = base;
- base = '/';
- }
-
- filename = path.resolve(base, filename);
- var ext = path.extname(filename);
- filename = path.join(path.dirname(filename), path.basename(filename, ext));
-
- var _filename = filename+ext;
-
- var i = 0;
- while (fs.existsSync(filename)) {
- _filename = filename+'_'+i+ext;
- i = i + 1;
- }
-
- return path.relative(base, _filename);
-}
-
-
-// List files in a directory
-function listFiles(root, options) {
- options = _.defaults(options || {}, {
- ignoreFiles: [],
- ignoreRules: []
- });
-
- var d = Q.defer();
-
- // Our list of files
- var files = [];
-
- var ig = Ignore({
- path: root,
- ignoreFiles: options.ignoreFiles
- });
-
- // Add extra rules to ignore common folders
- ig.addIgnoreRules(options.ignoreRules, '__custom_stuff');
-
- // Push each file to our list
- ig.on('child', function (c) {
- files.push(
- c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '')
- );
- });
-
- ig.on('end', function() {
- // Normalize paths on Windows
- if(process.platform === 'win32') {
- return d.resolve(files.map(function(file) {
- return file.replace(/\\/g, '/');
- }));
- }
-
- // Simply return paths otherwise
- return d.resolve(files);
- });
-
- ig.on('error', d.reject);
-
- return d.promise;
-}
-
-// Clean a folder without removing .git and .svn
-// Creates it if non existant
-function cleanFolder(root) {
- if (!fs.existsSync(root)) return fsUtils.mkdirp(root);
-
- return listFiles(root, {
- ignoreFiles: [],
- ignoreRules: [
- // Skip Git and SVN stuff
- '.git/',
- '.svn/'
- ]
- })
- .then(function(files) {
- var d = Q.defer();
-
- _.reduce(files, function(prev, file, i) {
- return prev.then(function() {
- var _file = path.join(root, file);
-
- d.notify({
- i: i+1,
- count: files.length,
- file: _file
- });
- return fsUtils.remove(_file);
- });
- }, Q())
- .then(function() {
- d.resolve();
- }, function(err) {
- d.reject(err);
- });
-
- return d.promise;
- });
-}
-
-// Find a file in a folder (case incensitive)
-// Return the real filename
-function findFile(root, filename) {
- return Q.nfcall(fs.readdir, root)
- .then(function(files) {
- return _.find(files, function(file) {
- return (file.toLowerCase() == filename.toLowerCase());
- });
- });
-}
-
-module.exports = fsUtils;
diff --git a/lib/utils/git.js b/lib/utils/git.js
deleted file mode 100644
index 72c881826..000000000
--- a/lib/utils/git.js
+++ /dev/null
@@ -1,127 +0,0 @@
-var Q = require('q');
-var _ = require('lodash');
-var path = require('path');
-var crc = require('crc');
-var exec = Q.denodeify(require('child_process').exec);
-var URI = require('urijs');
-var pathUtil = require('./path');
-
-var fs = require('./fs');
-
-var GIT_PREFIX = 'git+';
-var GIT_TMP = null;
-
-
-// Check if an url is a git dependency url
-function checkGitUrl(giturl) {
- return (giturl.indexOf(GIT_PREFIX) === 0);
-}
-
-// Validates a SHA in hexadecimal
-function validateSha(str) {
- return (/[0-9a-f]{40}/).test(str);
-}
-
-// Parse and extract infos
-function parseGitUrl(giturl) {
- var ref, uri, fileParts, filepath;
-
- if (!checkGitUrl(giturl)) return null;
- giturl = giturl.slice(GIT_PREFIX.length);
-
- uri = new URI(giturl);
- ref = uri.fragment() || 'master';
- uri.fragment(null);
-
- // Extract file inside the repo (after the .git)
- fileParts =uri.path().split('.git');
- filepath = fileParts.length > 1? fileParts.slice(1).join('.git') : '';
- if (filepath[0] == '/') filepath = filepath.slice(1);
-
- // Recreate pathname without the real filename
- uri.path(_.first(fileParts)+'.git');
-
- return {
- host: uri.toString(),
- ref: ref || 'master',
- filepath: filepath
- };
-}
-
-// Clone a git repo from a specific ref
-function cloneGitRepo(host, ref) {
- var isBranch = false;
-
- ref = ref || 'master';
- if (!validateSha(ref)) isBranch = true;
-
- return Q()
-
- // Create temporary folder to store git repos
- .then(function() {
- if (GIT_TMP) return;
- return fs.tmp.dir()
- .then(function(_tmp) {
- GIT_TMP = _tmp;
- });
- })
-
- // Return or clone the git repo
- .then(function() {
- // Unique ID for repo/ref combinaison
- var repoId = crc.crc32(host+'#'+ref).toString(16);
-
- // Absolute path to the folder
- var repoPath = path.resolve(GIT_TMP, repoId);
-
- return fs.exists(repoPath)
- .then(function(doExists) {
- if (doExists) return;
-
- // Clone repo
- return exec('git clone '+host+' '+repoPath)
- .then(function() {
- return exec('git checkout '+ref, { cwd: repoPath });
- });
- })
- .thenResolve(repoPath);
- });
-}
-
-// Get file from a git repo
-function resolveFileFromGit(giturl) {
- if (_.isString(giturl)) giturl = parseGitUrl(giturl);
- if (!giturl) return Q(null);
-
- // Clone or get from cache
- return cloneGitRepo(giturl.host, giturl.ref)
- .then(function(repo) {
-
- // Resolve relative path
- return path.resolve(repo, giturl.filepath);
- });
-}
-
-// Return root of git repo from a filepath
-function resolveGitRoot(filepath) {
- var relativeToGit, repoId;
-
- // No git repo cloned, or file is not in a git repository
- if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null;
-
- // Extract first directory (is the repo id)
- relativeToGit = path.relative(GIT_TMP, filepath);
- repoId = _.first(relativeToGit.split(path.sep));
- if (!repoId) return;
-
- // Return an absolute file
- return path.resolve(GIT_TMP, repoId);
-}
-
-
-module.exports = {
- checkUrl: checkGitUrl,
- parseUrl: parseGitUrl,
- resolveFile: resolveFileFromGit,
- resolveRoot: resolveGitRoot
-};
diff --git a/lib/utils/i18n.js b/lib/utils/i18n.js
deleted file mode 100644
index de64b49fd..000000000
--- a/lib/utils/i18n.js
+++ /dev/null
@@ -1,80 +0,0 @@
-var _ = require('lodash');
-var path = require('path');
-var fs = require('fs');
-
-var i18n = require('i18n');
-
-var I18N_PATH = path.resolve(__dirname, '../../theme/i18n/');
-var DEFAULT_LANGUAGE = 'en';
-var LOCALES = _.map(fs.readdirSync(I18N_PATH), function(lang) {
- return path.basename(lang, '.json');
-});
-
-i18n.configure({
- locales: LOCALES,
- directory: I18N_PATH,
- defaultLocale: DEFAULT_LANGUAGE,
- updateFiles: false
-});
-
-function compareLocales(lang, locale) {
- var langMain = _.first(lang.split('-'));
- var langSecond = _.last(lang.split('-'));
-
- var localeMain = _.first(locale.split('-'));
- var localeSecond = _.last(locale.split('-'));
-
- if (locale == lang) return 100;
- if (localeMain == langMain) return 50;
- if (localeSecond == langSecond) return 20;
- return 0;
-}
-
-var normalizeLanguage = _.memoize(function(lang) {
- var language = _.chain(LOCALES)
- .values()
- .map(function(locale) {
- return {
- locale: locale,
- score: compareLocales(lang, locale)
- };
- })
- .filter(function(lang) {
- return lang.score > 0;
- })
- .sortBy('score')
- .pluck('locale')
- .last()
- .value();
- return language || lang;
-});
-
-function translate(locale, phrase) {
- var args = Array.prototype.slice.call(arguments, 2);
-
- return i18n.__.apply({}, [{
- locale: locale,
- phrase: phrase
- }].concat(args));
-}
-
-function getCatalog(locale) {
- locale = normalizeLanguage(locale);
- return i18n.getCatalog(locale);
-}
-
-function getLocales() {
- return LOCALES;
-}
-
-function hasLocale(locale) {
- return _.contains(LOCALES, locale);
-}
-
-module.exports = {
- __: translate,
- normalizeLanguage: normalizeLanguage,
- getCatalog: getCatalog,
- getLocales: getLocales,
- hasLocale: hasLocale
-};
diff --git a/lib/utils/images.js b/lib/utils/images.js
deleted file mode 100644
index a82b0a153..000000000
--- a/lib/utils/images.js
+++ /dev/null
@@ -1,37 +0,0 @@
-var _ = require("lodash");
-var Q = require("q");
-var fs = require("./fs");
-var spawn = require("spawn-cmd").spawn;
-
-// Convert a svg file
-var convertSVG = function(source, dest, options) {
- if (!fs.existsSync(source)) return Q.reject(new Error("File doesn't exist: "+source));
- var d = Q.defer();
-
- options = _.defaults(options || {}, {
-
- });
-
- //var command = shellescape(["svgexport", source, dest]);
- var child = spawn("svgexport", [source, dest]);
-
- child.on("error", function(error) {
- if (error.code == "ENOENT") error = new Error("Need to install \"svgexport\" using \"npm install svgexport -g\"");
- return d.reject(error);
- });
-
- child.on("close", function(code) {
- if (code === 0 && fs.existsSync(dest)) {
- d.resolve();
- } else {
- d.reject(new Error("Error converting "+source+" into "+dest));
- }
- });
-
- return d.promise;
-};
-
-module.exports = {
- convertSVG: convertSVG,
- INVALID: [".svg"]
-};
diff --git a/lib/utils/links.js b/lib/utils/links.js
deleted file mode 100644
index 51223967e..000000000
--- a/lib/utils/links.js
+++ /dev/null
@@ -1,81 +0,0 @@
-var url = require('url');
-var path = require('path');
-
-// Is the link an external link
-function isExternal(href) {
- try {
- return Boolean(url.parse(href).protocol);
- } catch(err) {
- return false;
- }
-}
-
-// Return true if the link is relative
-function isRelative(href) {
- try {
- var parsed = url.parse(href);
-
- return !!(!parsed.protocol && parsed.path);
- } catch(err) {
- return true;
- }
-}
-
-// Return true if the link is an achor
-function isAnchor(href) {
- try {
- var parsed = url.parse(href);
- return !!(!parsed.protocol && !parsed.path && parsed.hash);
- } catch(err) {
- return false;
- }
-}
-
-// Normalize a path to be a link
-function normalizeLink(s) {
- return s.replace(/\\/g, '/');
-}
-
-// Relative to absolute path
-// dir: directory parent of the file currently in rendering process
-// outdir: directory parent from the html output
-function toAbsolute(_href, dir, outdir) {
- if (isExternal(_href)) return _href;
-
- // Path "_href" inside the base folder
- var hrefInRoot = path.normalize(path.join(dir, _href));
- if (_href[0] == '/') hrefInRoot = path.normalize(_href.slice(1));
-
- // Make it relative to output
- _href = path.relative(outdir, hrefInRoot);
-
- // Normalize windows paths
- _href = normalizeLink(_href);
-
- return _href;
-}
-
-// Join links
-function join() {
- var _href = path.join.apply(path, arguments);
-
- return normalizeLink(_href);
-};
-
-// Change extension
-function changeExtension(filename, newext) {
- return path.join(
- path.dirname(filename),
- path.basename(filename, path.extname(filename))+newext
- );
-}
-
-module.exports = {
- isAnchor: isAnchor,
- isRelative: isRelative,
- isExternal: isExternal,
- toAbsolute: toAbsolute,
- join: join,
- changeExtension: changeExtension,
- normalize: normalizeLink
-};
diff --git a/lib/utils/logger.js b/lib/utils/logger.js
deleted file mode 100644
index db3d90eab..000000000
--- a/lib/utils/logger.js
+++ /dev/null
@@ -1,102 +0,0 @@
-var _ = require("lodash");
-var util = require("util");
-var color = require("bash-color");
-
-var LEVELS = {
- DEBUG: 0,
- INFO: 1,
- WARN: 2,
- ERROR: 3,
- DISABLED: 10
-};
-
-var COLORS = {
- DEBUG: color.purple,
- INFO: color.cyan,
- WARN: color.yellow,
- ERROR: color.red
-};
-
-module.exports = function(_write, logLevel) {
- var logger = {};
- var lastChar = "\n";
- if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
-
- // Write a simple message
- logger.write = function(msg) {
- msg = msg.toString();
- lastChar = _.last(msg);
- return _write(msg);
- };
-
- // Format a message
- logger.format = function() {
- return util.format.apply(util, arguments);
- };
-
- // Write a line
- logger.writeLn = function(msg) {
- return this.write((msg || "")+"\n");
- };
-
- // Write a message with a certain level
- logger.log = function(level) {
- if (level < logLevel) return;
-
- var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
- var args = Array.prototype.slice.apply(arguments, [1]);
- var msg = logger.format.apply(logger, args);
-
- if (lastChar == "\n") {
- msg = COLORS[levelKey](levelKey.toLowerCase()+":")+" "+msg;
- }
-
- return logger.write(msg);
- };
- logger.logLn = function() {
- if (lastChar != "\n") logger.write("\n");
-
- var args = Array.prototype.slice.apply(arguments);
- args.push("\n");
- logger.log.apply(logger, args);
- };
-
- // Write a OK
- logger.ok = function(level) {
- var args = Array.prototype.slice.apply(arguments, [1]);
- var msg = logger.format.apply(logger, args);
- if (arguments.length > 1) {
- logger.logLn(level, color.green(">> ") + msg.trim().replace(/\n/g, color.green("\n>> ")));
- } else {
- logger.log(level, color.green("OK"), "\n");
- }
- };
-
- // Write an "FAIL"
- logger.fail = function(level) {
- return logger.log(level, color.red("ERROR")+"\n");
- };
-
- _.each(_.omit(LEVELS, "DISABLED"), function(level, levelKey) {
- levelKey = levelKey.toLowerCase();
-
- logger[levelKey] = _.partial(logger.log, level);
- logger[levelKey].ln = _.partial(logger.logLn, level);
- logger[levelKey].ok = _.partial(logger.ok, level);
- logger[levelKey].fail = _.partial(logger.fail, level);
- logger[levelKey].promise = function(p) {
- return p.
- then(function(st) {
- logger[levelKey].ok();
- return st;
- }, function(err) {
- logger[levelKey].fail();
- throw err;
- });
- };
- });
-
- return logger;
-};
-module.exports.LEVELS = LEVELS;
-module.exports.COLORS = COLORS;
diff --git a/lib/utils/navigation.js b/lib/utils/navigation.js
deleted file mode 100644
index d07eb35f6..000000000
--- a/lib/utils/navigation.js
+++ /dev/null
@@ -1,79 +0,0 @@
-var _ = require("lodash");
-
-// Cleans up an article/chapter object
-// remove "articles" attributes
-function clean(obj) {
- return obj && _.omit(obj, ["articles"]);
-}
-
-function flattenChapters(chapters) {
- return _.reduce(chapters, function(accu, chapter) {
- return accu.concat([clean(chapter)].concat(flattenChapters(chapter.articles)));
- }, []);
-}
-
-// Returns from a summary a map of
-/*
- {
- "file/path.md": {
- prev: ...,
- next: ...,
- },
- ...
- }
-*/
-function navigation(summary, files) {
- // Support single files as well as list
- files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
-
- // List of all navNodes
- // Flatten chapters
- var navNodes = flattenChapters(summary.chapters);
-
- // Mapping of prev/next for a give path
- var mapping = _.chain(navNodes)
- .map(function(current, i) {
- var prev = null, next = null;
-
- // Skip if no path
- if(!current.exists) return null;
-
- // Find prev
- prev = _.chain(navNodes.slice(0, i))
- .reverse()
- .find(function(node) {
- return node.exists && !node.external;
- })
- .value();
-
- // Find next
- next = _.chain(navNodes.slice(i+1))
- .find(function(node) {
- return node.exists && !node.external;
- })
- .value();
-
- return [current.path, {
- index: i,
- title: current.title,
- introduction: current.introduction,
- prev: prev,
- next: next,
- level: current.level,
- }];
- })
- .compact()
- .object()
- .value();
-
- // Filter for only files we want
- if(files) {
- return _.pick(mapping, files);
- }
-
- return mapping;
-}
-
-
-// Exports
-module.exports = navigation;
diff --git a/lib/utils/page.js b/lib/utils/page.js
deleted file mode 100644
index 010d703c0..000000000
--- a/lib/utils/page.js
+++ /dev/null
@@ -1,397 +0,0 @@
-var Q = require('q');
-var _ = require('lodash');
-var url = require('url');
-var path = require('path');
-var cheerio = require('cheerio');
-var domSerializer = require('dom-serializer');
-var request = require('request');
-var crc = require('crc');
-var slug = require('github-slugid');
-
-var links = require('./links');
-var imgUtils = require('./images');
-var fs = require('./fs');
-var batch = require('./batch');
-
-var parsableExtensions = require('gitbook-parsers').extensions;
-
-// Map of images that have been converted
-var imgConversionCache = {};
-
-// Render a cheerio dom as html
-function renderDom($, dom, options) {
- if (!dom && $._root && $._root.children) {
- dom = $._root.children;
- }
-
- options = options|| dom.options || $._options;
- return domSerializer(dom, options);
-}
-
-function replaceText($, el, search, replace, text_only ) {
- return $(el).each(function(){
- var node = this.firstChild,
- val,
- new_val,
-
- // Elements to be removed at the end.
- remove = [];
-
- // Only continue if firstChild exists.
- if ( node ) {
-
- // Loop over all childNodes.
- while (node) {
-
- // Only process text nodes.
- if ( node.nodeType === 3 ) {
-
- // The original node value.
- val = node.nodeValue;
-
- // The new value.
- new_val = val.replace( search, replace );
-
- // Only replace text if the new value is actually different!
- if ( new_val !== val ) {
-
- if ( !text_only && /\|\:])/g, '\\$1');
-}
-
-
-// Adapt an html snippet to be relative to a base folder
-function normalizeHtml(src, options) {
- var $ = cheerio.load(src, {
- // We should parse html without trying to normalize too much
- xmlMode: false,
-
- // SVG need some attributes to use uppercases
- lowerCaseAttributeNames: false,
- lowerCaseTags: false
- });
- var toConvert = [];
- var svgContent = {};
- var outputRoot = options.book.options.output;
-
- imgConversionCache[outputRoot] = imgConversionCache[outputRoot] || {};
-
- // Find svg images to extract and process
- if (options.convertImages) {
- $('svg').each(function() {
- var content = renderDom($, $(this));
- var svgId = _.uniqueId('svg');
- var dest = svgId+'.svg';
-
- // Generate filename
- dest = '/'+fs.getUniqueFilename(outputRoot, dest);
-
- svgContent[dest] = ''+content;
- $(this).replaceWith($(' ').attr('src', dest));
- });
- }
-
- // Generate ID for headings
- $('h1,h2,h3,h4,h5,h6').each(function() {
- if ($(this).attr('id')) return;
-
- $(this).attr('id', slug($(this).text()));
- });
-
- // Find images to normalize
- $('img').each(function() {
- var origin;
- var src = $(this).attr('src');
-
- if (!src) return;
- var isExternal = links.isExternal(src);
-
- // Transform as relative to the bases
- if (links.isRelative(src)) {
- src = links.toAbsolute(src, options.base, options.output);
- }
-
- // Convert if needed
- if (options.convertImages) {
- // If image is external and ebook, then downlaod the images
- if (isExternal) {
- origin = src;
- src = '/'+crc.crc32(origin).toString(16)+path.extname(url.parse(origin).pathname);
- src = links.toAbsolute(src, options.base, options.output);
- isExternal = false;
- }
-
- var ext = path.extname(src);
- var srcAbs = links.join('/', options.base, src);
-
- // Test image extension
- if (_.contains(imgUtils.INVALID, ext)) {
- if (imgConversionCache[outputRoot][srcAbs]) {
- // Already converted
- src = imgConversionCache[outputRoot][srcAbs];
- } else {
- // Not converted yet
- var dest = '';
-
- // Replace extension
- dest = links.join(path.dirname(srcAbs), path.basename(srcAbs, ext)+'.png');
- dest = dest[0] == '/'? dest.slice(1) : dest;
-
- // Get a name that doesn't exists
- dest = fs.getUniqueFilename(outputRoot, dest);
-
- options.book.log.debug.ln('detect invalid image (will be converted to png):', srcAbs);
-
- // Add to cache
- imgConversionCache[outputRoot][srcAbs] = '/'+dest;
-
- // Push to convert
- toConvert.push({
- origin: origin,
- content: svgContent[srcAbs],
- source: isExternal? srcAbs : path.join('./', srcAbs),
- dest: path.join('./', dest)
- });
-
- src = links.join('/', dest);
- }
-
- // Reset as relative to output
- src = links.toAbsolute(src, options.base, options.output);
- }
-
- else if (origin) {
- // Need to downlaod image
- toConvert.push({
- origin: origin,
- source: path.join('./', srcAbs)
- });
- }
- }
-
- $(this).attr('src', src);
- });
-
- // Normalize links
- $('a').each(function() {
- var href = $(this).attr('href');
- if (!href) return;
-
- if (links.isAnchor(href)) {
- // Keep it as it is
- } else if (links.isRelative(href)) {
- var parts = url.parse(href);
-
- var pathName = decodeURIComponent(parts.pathname);
- var anchor = parts.hash || '';
-
- // Calcul absolute path for this file (without the anchor)
- var absolutePath = links.join(options.base, pathName);
-
- // If is in navigation relative: transform as content
- if (options.navigation[absolutePath]) {
- absolutePath = options.book.contentLink(absolutePath);
- }
-
- // If md/adoc/rst files is not in summary
- // or for ebook, signal all files that are outside the summary
- else if (_.contains(parsableExtensions, path.extname(absolutePath)) ||
- _.contains(['epub', 'pdf', 'mobi'], options.book.options.generator)) {
- options.book.log.warn.ln('page', options.input, 'contains an hyperlink to resource outside spine \''+href+'\'');
- }
-
- // Transform as absolute
- href = links.toAbsolute('/'+absolutePath, options.base, options.output)+anchor;
- } else {
- // External links
- $(this).attr('target', '_blank');
- }
-
- // Transform extension
- $(this).attr('href', href);
- });
-
- // Highlight code blocks
- $('code').each(function() {
- // Normalize language
- var lang = _.chain(
- ($(this).attr('class') || '').split(' ')
- )
- .map(function(cl) {
- // Markdown
- if (cl.search('lang-') === 0) return cl.slice('lang-'.length);
-
- // Asciidoc
- if (cl.search('language-') === 0) return cl.slice('language-'.length);
-
- return null;
- })
- .compact()
- .first()
- .value();
-
- var source = $(this).text();
- var blk = options.book.template.applyBlock('code', {
- body: source,
- kwargs: {
- language: lang
- }
- });
-
- if (blk.html === false) $(this).text(blk.body);
- else $(this).html(blk.body);
- });
-
- // Replace glossary terms
- var glossary = _.sortBy(options.glossary, function(term) {
- return -term.name.length;
- });
-
- _.each(glossary, function(term) {
- var r = new RegExp( '\\b(' + pregQuote(term.name.toLowerCase()) + ')\\b' , 'gi' );
- var includedInFiles = false;
-
- $('*').each(function() {
- // Ignore codeblocks
- if (_.contains(['code', 'pre', 'a', 'script'], this.name.toLowerCase())) return;
-
- replaceText($, this, r, function(match) {
- // Add to files index in glossary
- if (!includedInFiles) {
- includedInFiles = true;
- term.files = term.files || [];
- term.files.push(options.navigation[options.input]);
- }
- return ''+match+' ';
- });
- });
- });
-
- return {
- html: renderDom($),
- images: toConvert
- };
-}
-
-// Convert svg images to png
-function convertImages(images, options) {
- if (!options.convertImages) return Q();
-
- var downloaded = [];
- options.book.log.debug.ln('convert ', images.length, 'images to png');
-
- return batch.execEach(images, {
- max: 100,
- fn: function(image) {
- var imgin = path.resolve(options.book.options.output, image.source);
-
- return Q()
-
- // Write image if need to be download
- .then(function() {
- if (!image.origin && !_.contains(downloaded, image.origin)) return;
- options.book.log.debug('download image', image.origin, '...');
- downloaded.push(image.origin);
- return options.book.log.debug.promise(fs.writeStream(imgin, request(image.origin)))
- .fail(function(err) {
- if (!_.isError(err)) err = new Error(err);
-
- err.message = 'Fail downloading '+image.origin+': '+err.message;
- throw err;
- });
- })
-
- // Write svg if content
- .then(function() {
- if (!image.content) return;
- return fs.writeFile(imgin, image.content);
- })
-
- // Convert
- .then(function() {
- if (!image.dest) return;
- var imgout = path.resolve(options.book.options.output, image.dest);
- options.book.log.debug('convert image', image.source, 'to', image.dest, '...');
- return options.book.log.debug.promise(imgUtils.convertSVG(imgin, imgout));
- });
- }
- })
- .then(function() {
- options.book.log.debug.ok(images.length+' images converted with success');
- });
-}
-
-// Adapt page content to be relative to a base folder
-function normalizePage(sections, options) {
- options = _.defaults(options || {}, {
- // Current book
- book: null,
-
- // Do we need to convert svg?
- convertImages: false,
-
- // Current file path
- input: '.',
-
- // Navigation to use to transform path
- navigation: {},
-
- // Directory parent of the file currently in rendering process
- base: './',
-
- // Directory parent from the html output
- output: './',
-
- // Glossary terms
- glossary: []
- });
-
- // List of images to convert
- var toConvert = [];
-
- sections = _.map(sections, function(section) {
- if (section.type != 'normal') return section;
-
- var out = normalizeHtml(section.content, options);
-
- toConvert = toConvert.concat(out.images);
- section.content = out.html;
- return section;
- });
-
- return Q()
- .then(function() {
- toConvert = _.uniq(toConvert, 'source');
- return convertImages(toConvert, options);
- })
- .thenResolve(sections);
-}
-
-
-module.exports = {
- normalize: normalizePage
-};
diff --git a/lib/utils/path.js b/lib/utils/path.js
index 5285896fb..dc97d5da3 100644
--- a/lib/utils/path.js
+++ b/lib/utils/path.js
@@ -1,5 +1,5 @@
-var _ = require("lodash");
-var path = require("path");
+var _ = require('lodash');
+var path = require('path');
// Return true if file path is inside a folder
function isInRoot(root, filename) {
@@ -26,8 +26,8 @@ function resolveInRoot(root) {
result = path.resolve(root, input);
if (!isInRoot(root, result)) {
- err = new Error("EACCESS: \"" + result + "\" not in \"" + root + "\"");
- err.code = "EACCESS";
+ err = new Error('EACCESS: "' + result + '" not in "' + root + '"');
+ err.code = 'EACCESS';
throw err;
}
diff --git a/lib/utils/progress.js b/lib/utils/progress.js
deleted file mode 100644
index 8dda89242..000000000
--- a/lib/utils/progress.js
+++ /dev/null
@@ -1,55 +0,0 @@
-var _ = require('lodash');
-
-// Returns from a navigation and a current file, a snapshot of current detailed state
-function calculProgress(navigation, current) {
- var n = _.size(navigation);
- var percent = 0, prevPercent = 0, currentChapter = null;
- var done = true;
-
- var chapters = _.chain(navigation)
-
- // Transform as array
- .map(function(nav, path) {
- nav.path = path;
- return nav;
- })
-
- // Sort entries
- .sortBy(function(nav) {
- return nav.index;
- })
-
- .map(function(nav, i) {
- // Calcul percent
- nav.percent = (i * 100) / Math.max((n - 1), 1);
-
- // Is it done
- nav.done = done;
- if (nav.path == current) {
- currentChapter = nav;
- percent = nav.percent;
- done = false;
- } else if (done) {
- prevPercent = nav.percent;
- }
-
- return nav;
- })
- .value();
-
- return {
- // Previous percent
- prevPercent: prevPercent,
-
- // Current percent
- percent: percent,
-
- // List of chapter with progress
- chapters: chapters,
-
- // Current chapter
- current: currentChapter
- };
-}
-
-module.exports = calculProgress;
diff --git a/lib/utils/server.js b/lib/utils/server.js
deleted file mode 100644
index 1d6822fa5..000000000
--- a/lib/utils/server.js
+++ /dev/null
@@ -1,94 +0,0 @@
-var Q = require("q");
-var events = require("events");
-var http = require("http");
-var send = require("send");
-var util = require("util");
-var url = require("url");
-
-var Server = function() {
- this.running = null;
- this.dir = null;
- this.port = 0;
- this.sockets = [];
-};
-util.inherits(Server, events.EventEmitter);
-
-// Return true if the server is running
-Server.prototype.isRunning = function() {
- return !!this.running;
-};
-
-// Stop the server
-Server.prototype.stop = function() {
- var that = this;
- if (!this.isRunning()) return Q();
-
- var d = Q.defer();
- this.running.close(function(err) {
- that.running = null;
- that.emit("state", false);
-
- if (err) d.reject(err);
- else d.resolve();
- });
-
- for (var i = 0; i < this.sockets.length; i++) {
- this.sockets[i].destroy();
- }
-
- return d.promise;
-};
-
-Server.prototype.start = function(dir, port) {
- var that = this, pre = Q();
- port = port || 8004;
-
- if (that.isRunning()) pre = this.stop();
- return pre
- .then(function() {
- var d = Q.defer();
-
- that.running = http.createServer(function(req, res){
- // Render error
- function error(err) {
- res.statusCode = err.status || 500;
- res.end(err.message);
- }
-
- // Redirect to directory"s index.html
- function redirect() {
- res.statusCode = 301;
- res.setHeader("Location", req.url + "/");
- res.end("Redirecting to " + req.url + "/");
- }
-
- // Send file
- send(req, url.parse(req.url).pathname)
- .root(dir)
- .on("error", error)
- .on("directory", redirect)
- .pipe(res);
- });
-
- that.running.on("connection", function (socket) {
- that.sockets.push(socket);
- socket.setTimeout(4000);
- socket.on("close", function () {
- that.sockets.splice(that.sockets.indexOf(socket), 1);
- });
- });
-
- that.running.listen(port, function(err) {
- if (err) return d.reject(err);
-
- that.port = port;
- that.dir = dir;
- that.emit("state", true);
- d.resolve();
- });
-
- return d.promise;
- });
-};
-
-module.exports = Server;
diff --git a/lib/utils/string.js b/lib/utils/string.js
deleted file mode 100644
index caa236428..000000000
--- a/lib/utils/string.js
+++ /dev/null
@@ -1,27 +0,0 @@
-var _ = require("lodash");
-
-function escapeShellArg(arg) {
- var ret = "";
-
- ret = arg.replace(/"/g, '\\"');
-
- return "\"" + ret + "\"";
-}
-
-function optionsToShellArgs(options) {
- return _.chain(options)
- .map(function(value, key) {
- if (value === null || value === undefined || value === false) return null;
- if (value === true) return key;
- return key+"="+escapeShellArg(value);
- })
- .compact()
- .value()
- .join(" ");
-}
-
-module.exports = {
- escapeShellArg: escapeShellArg,
- optionsToShellArgs: optionsToShellArgs,
- toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase)
-};
diff --git a/lib/utils/watch.js b/lib/utils/watch.js
deleted file mode 100644
index 4d1a752d4..000000000
--- a/lib/utils/watch.js
+++ /dev/null
@@ -1,40 +0,0 @@
-var Q = require("q");
-var _ = require("lodash");
-var path = require("path");
-var chokidar = require("chokidar");
-
-var parsers = require("gitbook-parsers");
-
-function watch(dir) {
- var d = Q.defer();
- dir = path.resolve(dir);
-
- var toWatch = [
- "book.json", "book.js"
- ];
-
- _.each(parsers.extensions, function(ext) {
- toWatch.push("**/*"+ext);
- });
-
- var watcher = chokidar.watch(toWatch, {
- cwd: dir,
- ignored: "_book/**",
- ignoreInitial: true
- });
-
- watcher.once("all", function(e, filepath) {
- watcher.close();
-
- d.resolve(filepath);
- });
- watcher.once("error", function(err) {
- watcher.close();
-
- d.reject(err);
- });
-
- return d.promise;
-}
-
-module.exports = watch;
diff --git a/package.json b/package.json
index 0d11bca30..14f9f2411 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,9 @@
"jsonschema": "1.0.2",
"json-schema-defaults": "0.1.1",
"merge-defaults": "0.2.1",
- "github-slugid": "1.0.0"
+ "github-slugid": "1.0.0",
+ "destroy": "1.0.4",
+ "ignore": "2.2.19"
},
"devDependencies": {
"eslint": "1.5.0",
diff --git a/test/assertions.js b/test/assertions.js
deleted file mode 100644
index f9c4ba394..000000000
--- a/test/assertions.js
+++ /dev/null
@@ -1,69 +0,0 @@
-var _ = require('lodash');
-var fs = require('fs');
-var path = require('path');
-var should = require('should');
-var cheerio = require('cheerio');
-
-require('should-promised');
-
-should.Assertion.add('file', function(file, description) {
- this.params = { actual: this.obj.toString(), operator: 'have file ' + file, message: description };
-
- this.obj.should.have.property('options').which.is.an.Object();
- this.obj.options.should.have.property('output').which.is.a.String();
- this.assert(fs.existsSync(path.resolve(this.obj.options.output, file)));
-});
-
-should.Assertion.add('jsonfile', function(file, description) {
- this.params = { actual: this.obj.toString(), operator: 'have valid jsonfile ' + file, message: description };
-
- this.obj.should.have.property('options').which.is.an.Object();
- this.obj.options.should.have.property('output').which.is.a.String();
- this.assert(JSON.parse(fs.readFileSync(path.resolve(this.obj.options.output, file), { encoding: 'utf-8' })));
-});
-
-should.Assertion.add('html', function(rules, description) {
- this.params = { actual: 'HTML string', operator: 'valid html', message: description };
- var $ = cheerio.load(this.obj);
-
- _.each(rules, function(validations, query) {
- validations = _.defaults(validations || {}, {
- // Select a specific element in the list of matched elements
- index: null,
-
- // Check that there is the correct count of elements
- count: 1,
-
- // Check attribute values
- attributes: {},
-
- // Trim inner text
- trim: false,
-
- // Check inner text
- text: undefined
- });
-
- var $el = $(query);
-
- // Select correct element
- if (_.isNumber(validations.index)) $el = $($el.get(validations.index));
-
- // Test number of elements
- $el.length.should.be.equal(validations.count);
-
- // Test text
- if (validations.text !== undefined) {
- var text = $el.text();
- if (validations.trim) text = text.trim();
- text.should.be.equal(validations.text);
- }
-
- // Test attributes
- _.each(validations.attributes, function(value, name) {
- var attr = $el.attr(name);
- should(attr).be.ok();
- attr.should.be.equal(value);
- });
- });
-});
diff --git a/test/books/basic/README.md b/test/books/basic/README.md
deleted file mode 100644
index 09ade4067..000000000
--- a/test/books/basic/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Readme
-
-Default description for the book.
diff --git a/test/books/basic/SUMMARY.md b/test/books/basic/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/basic/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/config-js/README.md b/test/books/config-js/README.md
deleted file mode 100644
index f3954314c..000000000
--- a/test/books/config-js/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Readme
diff --git a/test/books/config-js/SUMMARY.md b/test/books/config-js/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/config-js/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/config-js/book.js b/test/books/config-js/book.js
deleted file mode 100644
index 8731343c2..000000000
--- a/test/books/config-js/book.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- "title": "js-config"
-};
diff --git a/test/books/config-json/README.md b/test/books/config-json/README.md
deleted file mode 100644
index f3954314c..000000000
--- a/test/books/config-json/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Readme
diff --git a/test/books/config-json/SUMMARY.md b/test/books/config-json/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/config-json/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/config-json/book.json b/test/books/config-json/book.json
deleted file mode 100644
index eda10bb88..000000000
--- a/test/books/config-json/book.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "title": "json-config"
-}
diff --git a/test/books/conrefs/README.md b/test/books/conrefs/README.md
deleted file mode 100644
index 324ee1f69..000000000
--- a/test/books/conrefs/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Readme
-
-$$test_block1$$
-{% include "./block.md" %}
-
-### Relative
-
-{% include "./hello.md" %}
-{% include "/hello.md" %}
-
-### Git
-
-{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}
-{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}
-{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}
diff --git a/test/books/conrefs/SUMMARY.md b/test/books/conrefs/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/conrefs/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/conrefs/block.md b/test/books/conrefs/block.md
deleted file mode 100644
index 3910cb608..000000000
--- a/test/books/conrefs/block.md
+++ /dev/null
@@ -1 +0,0 @@
-$$test_block2$$
diff --git a/test/books/conrefs/hello.md b/test/books/conrefs/hello.md
deleted file mode 100644
index 557db03de..000000000
--- a/test/books/conrefs/hello.md
+++ /dev/null
@@ -1 +0,0 @@
-Hello World
diff --git a/test/books/glossary/GLOSSARY.md b/test/books/glossary/GLOSSARY.md
deleted file mode 100644
index 18840d246..000000000
--- a/test/books/glossary/GLOSSARY.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Glossary
-
-## test
-
-Just a simple and easy to understand test.
-
-## hakunamatata
-
-This word is not present in the content.
-
-## test long
-
-This is a test with a longer text that the first entry to test order.
diff --git a/test/books/glossary/README.md b/test/books/glossary/README.md
deleted file mode 100644
index d46da8bd2..000000000
--- a/test/books/glossary/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Readme
-
-The word test should be replaced by a glossary link.
-
-```
-But the word test should not be replaced in code blocks
-```
-
-The two words test long should be replaced by the correct glossary links.
-
diff --git a/test/books/glossary/SUMMARY.md b/test/books/glossary/SUMMARY.md
deleted file mode 100644
index deaea2031..000000000
--- a/test/books/glossary/SUMMARY.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Summary
-
-* [Page](folder/PAGE.md)
diff --git a/test/books/glossary/folder/PAGE.md b/test/books/glossary/folder/PAGE.md
deleted file mode 100644
index 33b17c2f8..000000000
--- a/test/books/glossary/folder/PAGE.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Page
-
-Just a page in a sub-directory to test relative link to glossary.
diff --git a/test/books/headings/README.md b/test/books/headings/README.md
deleted file mode 100644
index b08c485e1..000000000
--- a/test/books/headings/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Hello World
-
-## Hello {#hello-custom}
diff --git a/test/books/headings/SUMMARY.md b/test/books/headings/SUMMARY.md
deleted file mode 100644
index e69de29bb..000000000
diff --git a/test/books/highlight/README.md b/test/books/highlight/README.md
deleted file mode 100644
index f47ac835c..000000000
--- a/test/books/highlight/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Readme
-
-Block without language
-
-```
-test 1
-```
-
-Block with a language
-
-```lang
-test 2
-```
-
-Inline code: `test 3`
-Inline code with html: ``
\ No newline at end of file
diff --git a/test/books/highlight/SUMMARY.md b/test/books/highlight/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/highlight/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/images/README.md b/test/books/images/README.md
deleted file mode 100644
index 484f4101b..000000000
--- a/test/books/images/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# Tests for Images
-
-# SVG relative image
-
-
-### Convert SVG to PNG
-
-
-### Should only convert it once
-
-
-### Download remote images
-
-
-### Remote images with same filename
-
-
-
diff --git a/test/books/images/SUMMARY.md b/test/books/images/SUMMARY.md
deleted file mode 100644
index ec0c4fca4..000000000
--- a/test/books/images/SUMMARY.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Summary
-
-* [Page](./folder/PAGE.md)
diff --git a/test/books/images/folder/PAGE.md b/test/books/images/folder/PAGE.md
deleted file mode 100644
index 8beb060ef..000000000
--- a/test/books/images/folder/PAGE.md
+++ /dev/null
@@ -1,3 +0,0 @@
-### Images from other folder
-
-
diff --git a/test/books/images/test.svg b/test/books/images/test.svg
deleted file mode 100644
index 48bba7061..000000000
--- a/test/books/images/test.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- basic SVG tiny doc
-
- hello world
-
- Revision: 1.1
-
-
-
diff --git a/test/books/init/.gitignore b/test/books/init/.gitignore
deleted file mode 100644
index 8a88b2a94..000000000
--- a/test/books/init/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*
-!SUMMARY.md
-!.gitignore
diff --git a/test/books/init/SUMMARY.md b/test/books/init/SUMMARY.md
deleted file mode 100644
index 31c1561d6..000000000
--- a/test/books/init/SUMMARY.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Summary
-
-* [Hello](hello.md)
-* [Hello 2](hello2.md)
-* Hello 3
- * [Hello 4](hello3/hello4.md)
- * Hello 5
- * [Hello 6](hello3/hello5/hello6.md)
-
diff --git a/test/books/languages/LANGS.md b/test/books/languages/LANGS.md
deleted file mode 100644
index 4267b3c4b..000000000
--- a/test/books/languages/LANGS.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Language
-
-* [English](en)
-* [French](fr)
diff --git a/test/books/languages/README.md b/test/books/languages/README.md
deleted file mode 100644
index f3954314c..000000000
--- a/test/books/languages/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Readme
diff --git a/test/books/languages/en/README.md b/test/books/languages/en/README.md
deleted file mode 100644
index e965047ad..000000000
--- a/test/books/languages/en/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Hello
diff --git a/test/books/languages/en/SUMMARY.md b/test/books/languages/en/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/languages/en/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/languages/fr/README.md b/test/books/languages/fr/README.md
deleted file mode 100644
index 632e4fe73..000000000
--- a/test/books/languages/fr/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Bonjour
diff --git a/test/books/languages/fr/SUMMARY.md b/test/books/languages/fr/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/languages/fr/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/links/README.md b/test/books/links/README.md
deleted file mode 100644
index f3954314c..000000000
--- a/test/books/links/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Readme
diff --git a/test/books/links/SUMMARY.md b/test/books/links/SUMMARY.md
deleted file mode 100644
index a7debc22a..000000000
--- a/test/books/links/SUMMARY.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Summary
-
-* [Folder1](folder1/README.md)
-* [Folder2](folder2/README.md)
\ No newline at end of file
diff --git a/test/books/links/folder1/README.md b/test/books/links/folder1/README.md
deleted file mode 100644
index 99a83f667..000000000
--- a/test/books/links/folder1/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Folder 1
-
-[Link to folder2](../folder2/README.md)
diff --git a/test/books/links/folder2/README.md b/test/books/links/folder2/README.md
deleted file mode 100644
index aa2056ab6..000000000
--- a/test/books/links/folder2/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Folder 2
diff --git a/test/books/structure/README.adoc b/test/books/structure/README.adoc
deleted file mode 100644
index 354647f02..000000000
--- a/test/books/structure/README.adoc
+++ /dev/null
@@ -1 +0,0 @@
-== Readme for the bookk
diff --git a/test/books/structure/README.md b/test/books/structure/README.md
deleted file mode 100644
index 94f18a813..000000000
--- a/test/books/structure/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Readme for GitHub
diff --git a/test/books/structure/SUMMARY.md b/test/books/structure/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/structure/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/structure/book.json b/test/books/structure/book.json
deleted file mode 100644
index 110e0ba35..000000000
--- a/test/books/structure/book.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "structure": {
- "readme": "README.adoc"
- }
-}
\ No newline at end of file
diff --git a/test/books/structure/glossary.md b/test/books/structure/glossary.md
deleted file mode 100644
index 8c6c0fd77..000000000
--- a/test/books/structure/glossary.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Glossary
-
-### Hello
-
-Hello world
diff --git a/test/books/style-print/README.md b/test/books/style-print/README.md
deleted file mode 100644
index 09ade4067..000000000
--- a/test/books/style-print/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Readme
-
-Default description for the book.
diff --git a/test/books/style-print/SUMMARY.md b/test/books/style-print/SUMMARY.md
deleted file mode 100644
index ac9323cbc..000000000
--- a/test/books/style-print/SUMMARY.md
+++ /dev/null
@@ -1 +0,0 @@
-# Summary
diff --git a/test/books/style-print/styles/print.css b/test/books/style-print/styles/print.css
deleted file mode 100644
index b05faf8ea..000000000
--- a/test/books/style-print/styles/print.css
+++ /dev/null
@@ -1,3 +0,0 @@
-body {
- color: red;
-}
diff --git a/test/books/summary/PAGE1.md b/test/books/summary/PAGE1.md
deleted file mode 100644
index 960800143..000000000
--- a/test/books/summary/PAGE1.md
+++ /dev/null
@@ -1 +0,0 @@
-# Page 1
diff --git a/test/books/summary/README.md b/test/books/summary/README.md
deleted file mode 100644
index f3954314c..000000000
--- a/test/books/summary/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# Readme
diff --git a/test/books/summary/SUMMARY.md b/test/books/summary/SUMMARY.md
deleted file mode 100644
index 38d0f6b69..000000000
--- a/test/books/summary/SUMMARY.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Summary
-
-* [Page 1](./PAGE1.md)
-* [Page 2](./folder/PAGE2.md)
-* [Don't exists](./NOTFOUND.md)
diff --git a/test/books/summary/folder/PAGE2.md b/test/books/summary/folder/PAGE2.md
deleted file mode 100644
index f310be332..000000000
--- a/test/books/summary/folder/PAGE2.md
+++ /dev/null
@@ -1 +0,0 @@
-# Page 2
diff --git a/test/codehighlighting.js b/test/codehighlighting.js
deleted file mode 100644
index f167980a6..000000000
--- a/test/codehighlighting.js
+++ /dev/null
@@ -1,65 +0,0 @@
-var path = require('path');
-var fs = require('fs');
-
-var Plugin = require('../lib/plugin');
-var PLUGINS_ROOT = path.resolve(__dirname, 'plugins');
-
-describe('Code Highlighting', function () {
- var book, PAGE;
-
- before(function() {
- return books.generate('highlight', 'website', {
- prepare: function(_book) {
- book = _book;
-
- var plugin = new Plugin(book, 'replace_highlight');
- plugin.load('./replace_highlight', PLUGINS_ROOT);
-
- book.plugins.load(plugin);
- }
- })
- .then(function() {
- PAGE = fs.readFileSync(
- path.join(book.options.output, 'index.html'),
- { encoding: 'utf-8' }
- );
- });
- });
-
- it('should correctly replace highlighting', function() {
- PAGE.should.be.html({
- 'code': {
- index: 0,
- text: 'code_test 1\n_code'
- }
- });
- });
-
- it('should correctly replace highlighting with language', function() {
- PAGE.should.be.html({
- 'code': {
- index: 1,
- text: 'lang_test 2\n_lang'
- }
- });
- });
-
- it('should correctly replace highlighting for inline code', function() {
- PAGE.should.be.html({
- 'code': {
- index: 2,
- text: 'code_test 3_code'
- }
- });
- });
-
- it('should correctly replace highlighting for inline code with html tags', function() {
- PAGE.should.be.html({
- 'code': {
- index: 3,
- text: 'code__code'
- }
- });
- });
-});
-
diff --git a/test/config.js b/test/config.js
new file mode 100644
index 000000000..474ffea01
--- /dev/null
+++ b/test/config.js
@@ -0,0 +1,39 @@
+var mock = require('./mock');
+
+describe('Config', function() {
+
+ describe('config.load()', function() {
+ it('should not fail if no configuration file', function() {
+ return mock.setupDefaultBook()
+ .then(function(book) {
+ return book.prepareConfig();
+ });
+ });
+
+ it('should load from a JSON file', function() {
+ return mock.setupDefaultBook({
+ 'book.json': { title: 'Hello World' }
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+ .then(function() {
+ book.config.get('title', '').should.equal('Hello World');
+ });
+ });
+ });
+
+ it('should load from a JS file', function() {
+ return mock.setupDefaultBook({
+ 'book.js': 'module.exports = { title: "Hello World" };'
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+ .then(function() {
+ book.config.get('title', '').should.equal('Hello World');
+ });
+ });
+ });
+ });
+
+});
+
diff --git a/test/configuration.js b/test/configuration.js
deleted file mode 100644
index d30fd61e7..000000000
--- a/test/configuration.js
+++ /dev/null
@@ -1,37 +0,0 @@
-describe('Configuration', function () {
- it('should extract default title from README', function() {
- return books.parse('basic')
- .then(function(book) {
- book.options.title.should.be.equal('Readme');
- });
- });
-
- it('should extract default description from README', function() {
- return books.parse('basic')
- .then(function(book) {
- book.options.description.should.be.equal('Default description for the book.');
- });
- });
-
- it('should correctly load from json (book.json)', function() {
- return books.parse('config-json')
- .then(function(book) {
- book.options.title.should.be.equal('json-config');
- });
- });
-
- it('should correctly load from JavaScript (book.js)', function() {
- return books.parse('config-js')
- .then(function(book) {
- book.options.title.should.be.equal('js-config');
- });
- });
-
- it('should provide configuration on book.config.get', function() {
- return books.parse('basic')
- .then(function(book) {
- book.config.get('description').should.be.equal('Default description for the book.');
- book.getConfig('description').should.be.equal('Default description for the book.');
- });
- });
-});
diff --git a/test/conrefs.js b/test/conrefs.js
deleted file mode 100644
index 32e40584c..000000000
--- a/test/conrefs.js
+++ /dev/null
@@ -1,68 +0,0 @@
-var fs = require('fs');
-var path = require('path');
-
-describe('ConRefs', function () {
- var book, readme;
-
- before(function() {
- return books.generate('conrefs', 'website')
- .then(function(_book) {
- book = _book;
-
- readme = fs.readFileSync(
- path.join(book.options.output, 'index.html'),
- { encoding: 'utf-8' }
- );
- });
- });
-
- it('should handle local references', function() {
- readme.should.be.html({
- '.page-inner p#t1': {
- count: 1,
- text: 'Hello World',
- trim: true
- }
- });
- });
-
- it('should handle local references with absolute paths', function() {
- readme.should.be.html({
- '.page-inner p#t2': {
- count: 1,
- text: 'Hello World',
- trim: true
- }
- });
- });
-
- it('should correctly include file from git reference', function() {
- readme.should.be.html({
- '.page-inner p#t3': {
- count: 1,
- text: 'Hello from git',
- trim: true
- }
- });
- });
-
- it('should correctly handle deep include in git reference', function() {
- readme.should.be.html({
- '.page-inner p#t4': {
- count: 1,
- text: 'First Hello. Hello from git',
- trim: true
- }
- });
- });
-
- it('should correctly handle absolute include in git reference', function() {
- readme.should.be.html({
- '.page-inner p#t5': {
- count: 1,
- text: 'First Hello. Hello from git',
- trim: true
- }
- });
- });
-});
diff --git a/test/ebook.js b/test/ebook.js
deleted file mode 100644
index 5caf28b0e..000000000
--- a/test/ebook.js
+++ /dev/null
@@ -1,70 +0,0 @@
-var fs = require('fs');
-var path = require('path');
-
-describe('eBook generator', function () {
- describe('Basic Book', function() {
- var book;
-
- before(function() {
- return books.generate('basic', 'ebook')
- .then(function(_book) {
- book = _book;
- });
- });
-
- it('should correctly output a SUMMARY.html', function() {
- book.should.have.file('SUMMARY.html');
- });
-
- it('should correctly copy assets', function() {
- book.should.have.file('gitbook');
- book.should.have.file('gitbook/ebook.css');
- });
-
- it('should not copy website assets', function() {
- book.should.not.have.file('gitbook/style.css');
- });
- });
-
- describe('Custom styles', function() {
- var book;
-
- before(function() {
- return books.generate('style-print', 'ebook')
- .then(function(_book) {
- book = _book;
- });
- });
-
- it('should correctly copy print.css', function() {
- book.should.have.file('styles');
- book.should.have.file('styles/print.css');
- });
-
- it('should remove default print.css', function() {
- var PAGE = fs.readFileSync(
- path.join(book.options.output, 'index.html'),
- { encoding: 'utf-8' }
- );
-
- // There are 2 styles (one from plugin-highlight and the new style)
- PAGE.should.be.html({
- 'link': {
- count: 2
- }
- });
-
- PAGE.should.be.html({
- 'link[href=\'./styles/print.css\']': {
- count: 1
- }
- });
-
- PAGE.should.be.html({
- 'link[href="gitbook/plugins/gitbook-plugin-highlight/ebook.css"]': {
- count: 1
- }
- });
- });
- });
-});
diff --git a/test/format.js b/test/format.js
deleted file mode 100644
index 2ec1a6f75..000000000
--- a/test/format.js
+++ /dev/null
@@ -1,11 +0,0 @@
-describe('Formatting', function () {
- it('should provide formatting with book.formatString', function() {
- return books.parse('basic')
- .then(function(book) {
- return book.formatString('markdown', 'this is a **test**');
- })
- .then(function(content) {
- content.should.equal('this is a test
\n');
- });
- });
-});
diff --git a/test/git.js b/test/git.js
deleted file mode 100644
index 6fd6b41c8..000000000
--- a/test/git.js
+++ /dev/null
@@ -1,31 +0,0 @@
-var should = require("should");
-var git = require("../lib/utils/git");
-
-describe("GIT parser and getter", function () {
- it("should correctly parse an https url", function() {
- var parts = git.parseUrl("git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md");
-
- should.exist(parts);
- parts.host.should.be.equal("https://gist.github.com/69ea4542e4c8967d2fa7.git");
- parts.ref.should.be.equal("master");
- parts.filepath.should.be.equal("test.md");
- });
-
- it("should correctly parse an https url with a reference", function() {
- var parts = git.parseUrl("git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#0.1.2");
-
- should.exist(parts);
- parts.host.should.be.equal("https://gist.github.com/69ea4542e4c8967d2fa7.git");
- parts.ref.should.be.equal("0.1.2");
- parts.filepath.should.be.equal("test.md");
- });
-
- it("should correctly parse an ssh url", function() {
- var parts = git.parseUrl("git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1");
-
- should.exist(parts);
- parts.host.should.be.equal("git@github.com:GitbookIO/gitbook.git");
- parts.ref.should.be.equal("e1594cde2c32e4ff48f6c4eff3d3d461743d74e1");
- parts.filepath.should.be.equal("directory/README.md");
- });
-});
diff --git a/test/glossary.js b/test/glossary.js
deleted file mode 100644
index f0f31a5e0..000000000
--- a/test/glossary.js
+++ /dev/null
@@ -1,87 +0,0 @@
-var fs = require("fs");
-var path = require("path");
-
-describe("Glossary", function () {
- describe("Parsing", function() {
- var book;
-
- before(function() {
- return books.parse("glossary")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly list items", function() {
- book.should.have.property("glossary");
- book.glossary.should.have.lengthOf(3);
- });
- });
-
- describe("Generation", function() {
- var book;
-
- before(function() {
- return books.generate("glossary", "website")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly generate a GLOSSARY.html", function() {
- book.should.have.file("GLOSSARY.html");
- });
-
- describe("Page Integration", function() {
- var readme, page;
-
- before(function() {
- readme = fs.readFileSync(
- path.join(book.options.output, "index.html"),
- { encoding: "utf-8" }
- );
- page = fs.readFileSync(
- path.join(book.options.output, "folder/PAGE.html"),
- { encoding: "utf-8" }
- );
- });
-
- it("should correctly replaced terms by links", function() {
- readme.should.be.html({
- ".page-inner a[href=\"GLOSSARY.html#test\"]": {
- count: 1,
- text: "test",
- attributes: {
- title: "Just a simple and easy to understand test."
- }
- }
- });
- });
-
- it("should correctly replaced terms by links (relative)", function() {
- page.should.be.html({
- ".page-inner a[href=\"../GLOSSARY.html#test\"]": {
- count: 1
- }
- });
- });
-
- it("should not replace terms in codeblocks", function() {
- readme.should.be.html({
- ".page-inner code a": {
- count: 0
- }
- });
- });
-
- it("should correctly select the longest term", function() {
- readme.should.be.html({
- ".page-inner a[href=\"GLOSSARY.html#test_long\"]": {
- count: 1,
- text: "test long"
- }
- });
- });
- });
- });
-});
diff --git a/test/heading.js b/test/heading.js
deleted file mode 100644
index f6d65c3fb..000000000
--- a/test/heading.js
+++ /dev/null
@@ -1,37 +0,0 @@
-var path = require('path');
-var fs = require('fs');
-
-describe('Headings', function () {
- var book, PAGE;
-
- before(function() {
- return books.generate('headings', 'website')
- .then(function(_book) {
- book = _book;
-
- PAGE = fs.readFileSync(
- path.join(book.options.output, 'index.html'),
- { encoding: 'utf-8' }
- );
- });
- });
-
- describe('IDs', function() {
- it('should correctly generate an ID', function() {
- PAGE.should.be.html({
- 'h1#hello-world': {
- count: 1
- }
- });
- });
-
- it('should correctly accept custom ID', function() {
- PAGE.should.be.html({
- 'h2#hello-custom': {
- count: 1
- }
- });
- });
- });
-});
-
diff --git a/test/helper.js b/test/helper.js
deleted file mode 100644
index bbe82de47..000000000
--- a/test/helper.js
+++ /dev/null
@@ -1,78 +0,0 @@
-var os = require('os');
-var path = require('path');
-var Q = require('q');
-var _ = require('lodash');
-
-var fsUtil = require('../lib/utils/fs');
-var Book = require('../').Book;
-var LOG_LEVELS = require('../').LOG_LEVELS;
-
-require('./assertions');
-
-
-var BOOKS = {};
-var TMPDIR = os.tmpdir();
-
-
-// Generate and return a book
-function generateBook(bookId, test, opts) {
- opts = _.defaults(opts || {}, {
- prepare: function() {}
- });
-
- return parseBook(bookId, test, opts)
- .then(function(book) {
-
- return Q(opts.prepare(book))
- .then(function() {
- return book.generate(test);
- })
- .thenResolve(book);
- });
-}
-
-// Generate and return a book
-function parseBook(bookId, test, opts) {
- opts = _.defaults(opts || {}, {
- testId: ''
- });
-
- test = test || 'website';
- var testId = [test, opts.testId].join('-');
-
- BOOKS[bookId] = BOOKS[bookId] || {};
- if (BOOKS[bookId][testId]) return Q(BOOKS[bookId][testId]);
-
- BOOKS[bookId][testId] = new Book(path.resolve(__dirname, 'books', bookId), {
- logLevel: LOG_LEVELS.DISABLED,
- config: {
- output: path.resolve(TMPDIR, bookId+'-'+testId)
- }
- });
-
- return BOOKS[bookId][testId].parse()
- .then(function() {
- return BOOKS[bookId][testId];
- });
-}
-
-
-global.books = {
- parse: parseBook,
- generate: generateBook
-};
-
-// Cleanup all tests
-after(function() {
- return _.chain(BOOKS)
- .map(function(types) {
- return _.values(types);
- })
- .flatten()
- .reduce(function(prev, book) {
- return prev.then(function() {
- return fsUtil.remove(book.options.output);
- });
- }, Q())
- .value();
-});
diff --git a/test/images.js b/test/images.js
deleted file mode 100644
index de4506642..000000000
--- a/test/images.js
+++ /dev/null
@@ -1,58 +0,0 @@
-var fs = require("fs");
-var _ = require("lodash");
-var path = require("path");
-var cheerio = require("cheerio");
-
-describe("Images", function () {
- var book, readme, $, $img, srcs;
-
- before(function() {
- return books.generate("images", "ebook")
- .then(function(_book) {
- book = _book;
-
- readme = fs.readFileSync(
- path.join(book.options.output, "index.html"),
- { encoding: "utf-8" }
- );
- $ = cheerio.load(readme);
- $img = $("img");
- srcs = $img.map(function() {
- return $(this).attr("src");
- });
- });
- });
-
- it("should detect all images", function() {
- _.uniq(srcs).should.have.lengthOf(4);
- });
-
- it("should keep image tags", function() {
- srcs.should.have.lengthOf(5);
- });
-
- it("should not have .svg files", function() {
- _.each(srcs, function(src) {
- path.extname(src).should.not.equal(".svg");
- });
- });
-
- it("should correctly convert svg images to png", function() {
- _.each(srcs, function(src) {
- book.should.have.file(src);
- });
- });
-
- it("should handle relative paths", function() {
- var PAGE = fs.readFileSync(
- path.join(book.options.output, "folder/PAGE.html"),
- { encoding: "utf-8" }
- );
-
- PAGE.should.be.html({
- "img[src=\"../test.png\"]": {
- count: 1
- }
- });
- });
-});
diff --git a/test/init.js b/test/init.js
deleted file mode 100644
index 625d77c4d..000000000
--- a/test/init.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var fs = require('fs');
-var path = require('path');
-var should = require('should');
-
-var Book = require('../').Book;
-var LOG_LEVELS = require('../').LOG_LEVELS;
-
-describe('Init Books', function () {
- var initRoot;
-
- before(function() {
- initRoot = path.resolve(__dirname, 'books/init');
- return Book.init(initRoot, {
- logLevel: LOG_LEVELS.DISABLED
- });
- });
-
- it('should create all chapters', function() {
- should(fs.existsSync(path.resolve(initRoot, 'hello.md'))).be.ok();
- should(fs.existsSync(path.resolve(initRoot, 'hello2.md'))).be.ok();
- should(fs.existsSync(path.resolve(initRoot, 'hello3/hello4.md'))).be.ok();
- should(fs.existsSync(path.resolve(initRoot, 'hello3/hello5/hello6.md'))).be.ok();
- });
-});
diff --git a/test/json.js b/test/json.js
deleted file mode 100644
index 60baf9ae9..000000000
--- a/test/json.js
+++ /dev/null
@@ -1,92 +0,0 @@
-var fs = require('fs');
-var path = require('path');
-
-describe('JSON generator', function () {
- describe('Basic Book', function() {
- var book;
-
- before(function() {
- return books.generate('basic', 'json')
- .then(function(_book) {
- book = _book;
- });
- });
-
- it('should correctly output a README.json', function() {
- book.should.have.file('README.json');
- });
-
- it('should output a valid json', function() {
- book.should.have.jsonfile('README.json');
- });
-
- describe('Page Format', function() {
- var page;
-
- before(function() {
- page = JSON.parse(
- fs.readFileSync(
- path.join(book.options.output, 'README.json'),
- { encoding: 'utf-8' }
- )
- );
- });
-
- it('should contains valid section', function() {
- page.should.have.property('sections').with.lengthOf(1);
- page.sections[0].should.have.property('content').which.is.a.String();
- page.sections[0].should.have.property('type', 'normal');
- });
-
- it('should contains valid progress', function() {
- page.should.have.property('progress');
- page.progress.should.have.property('chapters').with.lengthOf(1);
- page.progress.should.have.property('current');
- });
-
- it('should contains no languages', function() {
- page.should.have.property('langs').with.lengthOf(0);
- });
- });
- });
-
- describe('Multilingual Book', function() {
- var book;
-
- before(function() {
- return books.generate('languages', 'json')
- .then(function(_book) {
- book = _book;
- });
- });
-
- it('should correctly output READMEs', function() {
- book.should.have.file('README.json');
- book.should.have.file('en/README.json');
- book.should.have.file('fr/README.json');
- });
-
- it('should output valid json', function() {
- book.should.have.jsonfile('README.json');
- book.should.have.jsonfile('en/README.json');
- book.should.have.jsonfile('fr/README.json');
- });
-
- describe('Page Format', function() {
- var page;
-
- before(function() {
- page = JSON.parse(
- fs.readFileSync(
- path.join(book.options.output, 'README.json'),
- { encoding: 'utf-8' }
- )
- );
- });
-
- it('should contains no languages', function() {
- page.should.have.property('langs').with.lengthOf(2);
- });
- });
- });
-});
diff --git a/test/languages.js b/test/languages.js
deleted file mode 100644
index 0bde347cb..000000000
--- a/test/languages.js
+++ /dev/null
@@ -1,37 +0,0 @@
-describe("Languages", function () {
- describe("Parsing", function() {
- var book;
-
- before(function() {
- return books.parse("languages")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly list languages", function() {
- book.should.have.property("books");
- book.books.should.have.lengthOf(2);
-
- book.books[0].options.language.should.be.equal("en");
- book.books[1].options.language.should.be.equal("fr");
- });
- });
-
- describe("Generation", function() {
- var book;
-
- before(function() {
- return books.generate("languages", "website")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly create books", function() {
- book.should.have.file("index.html");
- book.should.have.file("en/index.html");
- book.should.have.file("fr/index.html");
- });
- });
-});
diff --git a/test/links.js b/test/links.js
deleted file mode 100644
index baca9d1b3..000000000
--- a/test/links.js
+++ /dev/null
@@ -1,63 +0,0 @@
-var fs = require("fs");
-var path = require("path");
-var cheerio = require("cheerio");
-
-var links = require("../lib/utils/links");
-
-describe("Links", function () {
- it("should correctly test external links", function() {
- links.isExternal("http://google.fr").should.be.exactly(true);
- links.isExternal("https://google.fr").should.be.exactly(true);
- links.isExternal("test.md").should.be.exactly(false);
- links.isExternal("folder/test.md").should.be.exactly(false);
- links.isExternal("/folder/test.md").should.be.exactly(false);
- });
-
- it("should correctly detect anchor links", function() {
- links.isAnchor("#test").should.be.exactly(true);
- links.isAnchor(" #test").should.be.exactly(true);
- links.isAnchor("https://google.fr#test").should.be.exactly(false);
- links.isAnchor("test.md#test").should.be.exactly(false);
- });
-
- describe("toAbsolute", function() {
- it("should correctly transform as absolute", function() {
- links.toAbsolute("http://google.fr").should.be.equal("http://google.fr");
- links.toAbsolute("test.md", "./", "./").should.be.equal("test.md");
- links.toAbsolute("folder/test.md", "./", "./").should.be.equal("folder/test.md");
- });
-
- it("should correctly handle windows path", function() {
- links.toAbsolute("folder\\test.md", "./", "./").should.be.equal("folder/test.md");
- });
-
- it("should correctly handle absolute path", function() {
- links.toAbsolute("/test.md", "./", "./").should.be.equal("test.md");
- links.toAbsolute("/test.md", "test", "test").should.be.equal("../test.md");
- links.toAbsolute("/sub/test.md", "test", "test").should.be.equal("../sub/test.md");
- });
- });
-
- describe("page", function() {
- var book;
-
- before(function() {
- return books.generate("links", "website")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly replace relative links", function() {
- var readme = fs.readFileSync(
- path.join(book.options.output, "folder1/index.html"),
- { encoding: "utf-8" }
- );
- var $ = cheerio.load(readme);
- var $a = $(".page-inner a");
-
- $a.attr("href").should.be.exactly("../folder2/index.html");
- });
- });
-
-});
diff --git a/test/mock.js b/test/mock.js
new file mode 100644
index 000000000..f562fa9b1
--- /dev/null
+++ b/test/mock.js
@@ -0,0 +1,61 @@
+var Q = require('q');
+var _ = require('lodash');
+var tmp = require('tmp');
+var path = require('path');
+
+require('should');
+require('should-promised');
+
+var Book = require('../').Book;
+var NodeFS = require('../lib/fs/node');
+
+// Create filesystem instance for testing
+var fs = new NodeFS();
+
+function setupFS(fs, rootFolder, files) {
+ return _.chain(_.pairs(files))
+ .sortBy(0)
+ .reduce(function(prev, pair) {
+ return prev.then(function() {
+ var filename = path.resolve(rootFolder, pair[0]);
+ var buf = pair[1];
+
+ if (_.isObject(buf)) buf = JSON.stringify(buf);
+ if (_.isString(buf)) buf = new Buffer(buf, 'utf-8');
+
+ return fs.write(filename, buf);
+ });
+ }, Q())
+ .value()
+ .then(function() {
+ return fs;
+ });
+}
+
+// Setup a mock book for testing using a map of files
+function setupBook(files, opts) {
+ opts = opts || {};
+
+ return Q.nfcall(tmp.dir.bind(tmp)).get(0)
+ .then(function(folder) {
+ opts.fs = fs;
+ opts.root = folder;
+ return setupFS(fs, folder, files);
+ })
+ .then(function(fs) {
+ return new Book(opts);
+ });
+}
+
+// Setup a book with default README/SUMMARY
+function setupDefaultBook(files, opts) {
+ return setupBook(_.defaults(files || {}, {
+ 'README.md': 'Hello',
+ 'SUMMARY.md': '# Summary'
+ }), opts);
+}
+
+module.exports = {
+ setupBook: setupBook,
+ setupDefaultBook: setupDefaultBook
+};
diff --git a/test/navigation.js b/test/navigation.js
deleted file mode 100644
index 9118b3c8e..000000000
--- a/test/navigation.js
+++ /dev/null
@@ -1,61 +0,0 @@
-var should = require("should");
-
-describe("Navigation", function () {
- var book;
-
- before(function() {
- return books.parse("summary")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly parse navigation as a map", function() {
- book.should.have.property("navigation");
- book.navigation.should.have.property("README.md");
- book.navigation.should.have.property("README.md");
- });
-
- it("should correctly include filenames", function() {
- book.navigation.should.have.property("README.md");
- book.navigation.should.have.property("PAGE1.md");
- book.navigation.should.have.property("folder/PAGE2.md");
- book.navigation.should.not.have.property("NOTFOUND.md");
- });
-
- it("should correctly detect next/prev for README", function() {
- var README = book.navigation["README.md"];
-
- README.index.should.equal(0);
- README.should.have.property("next");
- should(README.prev).not.be.ok();
-
- README.next.should.have.property("path");
- README.next.path.should.equal("PAGE1.md");
- });
-
- it("should correctly detect next/prev a page", function() {
- var PAGE = book.navigation["PAGE1.md"];
-
- PAGE.index.should.equal(1);
- PAGE.should.have.property("next");
- PAGE.should.have.property("prev");
-
- PAGE.prev.should.have.property("path");
- PAGE.prev.path.should.equal("README.md");
-
- PAGE.next.should.have.property("path");
- PAGE.next.path.should.equal("folder/PAGE2.md");
- });
-
- it("should correctly detect next/prev for last page", function() {
- var PAGE = book.navigation["folder/PAGE2.md"];
-
- PAGE.index.should.equal(2);
- PAGE.should.have.property("prev");
- should(PAGE.next).not.be.ok();
-
- PAGE.prev.should.have.property("path");
- PAGE.prev.path.should.equal("PAGE1.md");
- });
-});
diff --git a/test/parse.js b/test/parse.js
new file mode 100644
index 000000000..dca780e03
--- /dev/null
+++ b/test/parse.js
@@ -0,0 +1,24 @@
+var mock = require('./mock');
+
+describe('Parsing', function() {
+
+ it('should fail without SUMMARY', function() {
+ return mock.setupBook({
+ 'README.md': ''
+ })
+ .then(function(book) {
+ return book.parse().should.be.rejected;
+ });
+ });
+
+ it('should fail without README', function() {
+ return mock.setupBook({
+ 'SUMMARY.md': ''
+ })
+ .then(function(book) {
+ return book.parse().should.be.rejected;
+ });
+ });
+
+});
+
diff --git a/test/plugins.js b/test/plugins.js
deleted file mode 100644
index 1600d0dc1..000000000
--- a/test/plugins.js
+++ /dev/null
@@ -1,330 +0,0 @@
-var _ = require('lodash');
-var fs = require('fs');
-var should = require('should');
-var path = require('path');
-
-var Plugin = require('../lib/plugin');
-var parsers = require('gitbook-parsers');
-var PLUGINS_ROOT = path.resolve(__dirname, 'plugins');
-
-describe('Plugins', function () {
- var book;
-
- before(function() {
- return books.parse('basic')
- .then(function(_book) {
- book = _book;
- });
- });
-
- describe('Invalid', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'invalid');
- plugin.load('./invalid', PLUGINS_ROOT);
- });
-
- it('should be detected', function() {
- should(plugin.isValid()).be.exactly(false);
- });
- });
-
- describe('Empty', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'empty');
- plugin.load('./empty', PLUGINS_ROOT);
- });
-
- it('should valid a plugin', function() {
- should(plugin.isValid()).be.exactly(true);
- });
-
- it('should return an empty list of resources', function() {
- return plugin.getResources()
- .then(function(resources) {
- _.each(Plugin.RESOURCES, function(resName) {
- resources[resName].should.have.lengthOf(0);
- });
- });
- });
- });
-
- describe('Configuration', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'testconfig');
- plugin.load('./config', PLUGINS_ROOT);
- });
-
- it('should throw error for invalid configuration', function() {
- return plugin.validateConfig({})
- .should.be.rejectedWith('Configuration Error: pluginsConfig.testconfig.testRequired is required');
- });
-
- it('should throw error for invalid types', function() {
- return plugin.validateConfig({
- testRequired: 'hello'
- })
- .should.be.rejectedWith('Configuration Error: pluginsConfig.testconfig.testRequired is not of a type(s) number');
- });
-
- it('should extend with default values', function() {
- return plugin.validateConfig({
- testRequired: 12
- })
- .should.be.fulfilledWith({
- hello: 'world',
- testRequired: 12
- });
- });
- });
-
- describe('Resources', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'resources');
- plugin.load('./resources', PLUGINS_ROOT);
-
- return book.plugins.load(plugin);
- });
-
- it('should valid a plugin', function() {
- should(plugin.isValid()).be.exactly(true);
- });
-
- describe('Website', function() {
- it('should return a valid list of resources', function() {
- return plugin.getResources('website')
- .then(function(resources) {
- resources.js.should.have.lengthOf(1);
- });
- });
-
- it('should extend books plugins', function() {
- var resources = book.plugins.resources('website');
- resources.js.should.have.lengthOf(5);
- });
- });
-
- describe('eBook', function() {
- it('should return a valid list of resources', function() {
- return plugin.getResources('ebook')
- .then(function(resources) {
- resources.css.should.have.lengthOf(1);
- });
- });
-
- it('should extend books plugins', function() {
- var resources = book.plugins.resources('ebook');
-
- // There is resources from highlight plugin and this plugin
- resources.css.should.have.lengthOf(2);
- should.exist(_.find(resources.css, {
- path: 'gitbook-plugin-resources/test'
- }));
- });
- });
- });
-
- describe('Filters', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'filters');
- plugin.load('./filters', PLUGINS_ROOT);
-
- return book.plugins.load(plugin);
- });
-
- it('should valid a plugin', function() {
- should(plugin.isValid()).be.exactly(true);
- });
-
- it('should return a map of filters', function() {
- var filters = plugin.getFilters();
-
- _.size(filters).should.equal(2);
- filters.should.have.property('hello');
- filters.should.have.property('helloCtx');
- });
-
- it('should correctly extend template filters', function() {
- return book.template.renderString('{{ \'World\'|hello }}')
- .then(function(content) {
- content.should.equal('Hello World');
- });
- });
-
- it('should correctly set book as context', function() {
- return book.template.renderString('{{ \'root\'|helloCtx }}')
- .then(function(content) {
- content.should.equal('root:'+book.root);
- });
- });
- });
-
- describe('Blocks', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'blocks');
- plugin.load('./blocks', PLUGINS_ROOT);
-
- return book.plugins.load(plugin);
- });
-
- var testTpl = function(str, args, options) {
- return book.template.renderString(str, args, options)
- .then(book.template.postProcess);
- };
-
- it('should valid a plugin', function() {
- should(plugin.isValid()).be.exactly(true);
- });
-
- it('should correctly extend template blocks', function() {
- return testTpl('{% test %}hello{% endtest %}')
- .then(function(content) {
- content.should.equal('testhellotest');
- });
- });
-
- describe('Shortcuts', function() {
- it('should correctly accept shortcuts', function() {
- return testTpl('$$hello$$', {}, {
- type: 'markdown'
- })
- .then(function(content) {
- content.should.equal('testhellotest');
- });
- });
-
- it('should correctly apply shortcuts to included file', function() {
- return books.generate('conrefs', 'website', {
- testId: 'include-plugins',
- prepare: function(bookConref) {
- plugin = new Plugin(bookConref, 'blocks');
- plugin.load('./blocks', PLUGINS_ROOT);
-
- return bookConref.plugins.load(plugin);
- }
- })
- .then(function(bookConref) {
- var readme = fs.readFileSync(
- path.join(bookConref.options.output, 'index.html'),
- { encoding: 'utf-8' }
- );
-
- readme.should.be.html({
- '.page-inner p#test-plugin-block-shortcuts-1': {
- count: 1,
- text: 'testtest_block1test',
- trim: true
- },
- '.page-inner p#test-plugin-block-shortcuts-2': {
- count: 1,
- text: 'testtest_block2test',
- trim: true
- }
- });
- });
- });
- });
-
-
- it('should correctly extend template blocks with defined end', function() {
- return testTpl('{% test2 %}hello{% endtest2end %}')
- .then(function(content) {
- content.should.equal('test2hellotest2');
- });
- });
-
- it('should correctly extend template blocks with sub-blocks', function() {
- return testTpl('{% test3join separator=";" %}hello{% also %}world{% endtest3join %}')
- .then(function(content) {
- content.should.equal('hello;world');
- });
- });
-
- it('should correctly extend template blocks with different sub-blocks', function() {
- return testTpl('{% test4join separator=";" %}hello{% also %}the{% finally %}world{% endtest4join %}')
- .then(function(content) {
- content.should.equal('hello;the;world');
- });
- });
-
- it('should correctly extend template blocks with arguments (1)', function() {
- return testTpl('{% test5args "a" %}{% endtest5args %}')
- .then(function(content) {
- content.should.equal('test5atest5');
- });
- });
-
- it('should correctly extend template blocks with arguments (2)', function() {
- return testTpl('{% test5args "a", "b" %}{% endtest5args %}')
- .then(function(content) {
- content.should.equal('test5a,btest5');
- });
- });
-
- it('should correctly extend template blocks with arguments (3)', function() {
- return testTpl('{% test5args "a", "b", "c" %}{% endtest5args %}')
- .then(function(content) {
- content.should.equal('test5a,b,ctest5');
- });
- });
-
- it('should correctly extend template blocks with args and kwargs', function() {
- return testTpl('{% test5kwargs "a", "b", "c", d="test", e="test2" %}{% endtest5kwargs %}')
- .then(function(content) {
- content.should.equal('test5a,b,c,d:test,e:test2,__keywords:truetest5');
- });
- });
-
- it('should correctly extend template blocks with access to context', function() {
- return testTpl('{% set name = "john" %}{% test6context %}{% endtest6context %}', {})
- .then(function(content) {
- content.should.equal('test6johntest6');
- });
- });
- });
-
- describe('Blocks without parsing', function() {
- var plugin;
-
- before(function() {
- plugin = new Plugin(book, 'blocks');
- plugin.load('./blocks', PLUGINS_ROOT);
-
- return book.plugins.load(plugin);
- });
-
- var testTpl = function(markup, str, args, options) {
- var filetype = parsers.get(markup);
-
- return book.template.renderString(str, args, options)
- .then(filetype.page).get('sections').get(0).get('content')
- .then(book.template.postProcess);
- };
-
- it('should correctly process unparsable for markdown', function() {
- return testTpl('.md', '{% test %}**hello**{% endtest %}')
- .then(function(content) {
- content.should.equal('test**hello**test
\n');
- });
- });
-
- it('should correctly process unparsable for asciidoc', function() {
- return testTpl('.adoc', '{% test %}**hello**{% endtest %}')
- .then(function(content) {
- content.should.equal('');
- });
- });
- });
-});
-
diff --git a/test/plugins/blocks/index.js b/test/plugins/blocks/index.js
deleted file mode 100644
index 9bdbe864d..000000000
--- a/test/plugins/blocks/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-var assert = require("assert");
-
-module.exports = {
- blocks: {
- "test": {
- shortcuts: {
- parsers: ["markdown"],
- start: "$$",
- end: "$$"
- },
- process: function(blk) {
- return "test"+blk.body+"test";
- }
- },
- "test2": {
- end: "endtest2end",
- process: function(blk) {
- return "test2"+blk.body+"test2";
- }
- },
- "test3join": {
- blocks: [
- "also"
- ],
- process: function(blk) {
- return [blk.body, blk.blocks[0].body].join(blk.kwargs.separator);
- }
- },
- "test4join": {
- blocks: [
- "also", "finally"
- ],
- process: function(blk) {
- assert(blk.blocks.length, 2);
- assert(blk.blocks[0].name, "also");
- assert(blk.blocks[1].name, "finally");
- return [blk.body, blk.blocks[0].body, blk.blocks[1].body].join(blk.kwargs.separator);
- }
- },
- "test5args": {
- process: function(blk) {
- return "test5"+blk.args.join(",")+"test5";
- }
- },
- "test5kwargs": {
- process: function(blk) {
- var s = blk.args.join(",");
- for (var key in blk.kwargs) {
- s = s + ","+key+":"+blk.kwargs[key];
- }
-
- return "test5"+s+"test5";
- }
- },
- "test6context": {
- process: function() {
- return "test6"+(this.ctx.name)+"test6";
- }
- },
- }
-};
\ No newline at end of file
diff --git a/test/plugins/blocks/package.json b/test/plugins/blocks/package.json
deleted file mode 100644
index 7c41fd32a..000000000
--- a/test/plugins/blocks/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "gitbook-plugin-blocks",
- "description": "Test blocks",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "*"
- }
-}
\ No newline at end of file
diff --git a/test/plugins/config/index.js b/test/plugins/config/index.js
deleted file mode 100644
index f053ebf79..000000000
--- a/test/plugins/config/index.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = {};
diff --git a/test/plugins/config/package.json b/test/plugins/config/package.json
deleted file mode 100644
index 03ef744a4..000000000
--- a/test/plugins/config/package.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "gitbook-plugin-testconfig",
- "description": "Test plugin configuration",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "*"
- },
- "gitbook": {
- "properties": {
- "hello": {
- "type": "string",
- "default": "world"
- },
- "testRequired": {
- "type": "number",
- "required": true
- }
- }
- }
-}
\ No newline at end of file
diff --git a/test/plugins/empty/index.js b/test/plugins/empty/index.js
deleted file mode 100644
index a09954537..000000000
--- a/test/plugins/empty/index.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = {};
\ No newline at end of file
diff --git a/test/plugins/empty/package.json b/test/plugins/empty/package.json
deleted file mode 100644
index 78c7e727d..000000000
--- a/test/plugins/empty/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "gitbook-plugin-empty",
- "description": "Test empty plugin",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "*"
- }
-}
\ No newline at end of file
diff --git a/test/plugins/filters/index.js b/test/plugins/filters/index.js
deleted file mode 100644
index 2cf53b1ef..000000000
--- a/test/plugins/filters/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-module.exports = {
- filters: {
- hello: function(text) {
- return "Hello "+text;
- },
- helloCtx: function(text) {
- return text+":"+this.book.root;
- }
- }
-};
\ No newline at end of file
diff --git a/test/plugins/filters/package.json b/test/plugins/filters/package.json
deleted file mode 100644
index f1d4e4530..000000000
--- a/test/plugins/filters/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "gitbook-plugin-filters",
- "description": "Test filters",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "*"
- }
-}
\ No newline at end of file
diff --git a/test/plugins/invalid/index.js b/test/plugins/invalid/index.js
deleted file mode 100644
index a09954537..000000000
--- a/test/plugins/invalid/index.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = {};
\ No newline at end of file
diff --git a/test/plugins/invalid/package.json b/test/plugins/invalid/package.json
deleted file mode 100644
index da34090c2..000000000
--- a/test/plugins/invalid/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "gitbook-plugin-invalid",
- "description": "Test invalid plugin",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "<2.0.0"
- }
-}
\ No newline at end of file
diff --git a/test/plugins/replace_highlight/index.js b/test/plugins/replace_highlight/index.js
deleted file mode 100644
index 8586486d5..000000000
--- a/test/plugins/replace_highlight/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
- blocks: {
- "code": {
- process: function(blk) {
- var lang = blk.kwargs.language || "code";
-
- return {
- body: lang+"_"+blk.body+"_"+lang,
- html: false
- };
- }
- }
- }
-};
\ No newline at end of file
diff --git a/test/plugins/replace_highlight/package.json b/test/plugins/replace_highlight/package.json
deleted file mode 100644
index 72d1033db..000000000
--- a/test/plugins/replace_highlight/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "gitbook-plugin-replace_highlight",
- "description": "Test replacing default code highlighter",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "*"
- }
-}
\ No newline at end of file
diff --git a/test/plugins/resources/index.js b/test/plugins/resources/index.js
deleted file mode 100644
index bafa54b2e..000000000
--- a/test/plugins/resources/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports = {
- website: {
- js: [
- "https://cdn.mathjax.org/mathjax/2.4-latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"
- ]
- },
- ebook: {
- css: [
- "test"
- ]
- }
-};
diff --git a/test/plugins/resources/package.json b/test/plugins/resources/package.json
deleted file mode 100644
index ab4320d8b..000000000
--- a/test/plugins/resources/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "gitbook-plugin-resources",
- "description": "Test resources plugin",
- "main": "index.js",
- "version": "0.0.1",
- "engines": {
- "gitbook": "*"
- }
-}
\ No newline at end of file
diff --git a/test/readme.js b/test/readme.js
new file mode 100644
index 000000000..0ebb4903f
--- /dev/null
+++ b/test/readme.js
@@ -0,0 +1,34 @@
+var mock = require('./mock');
+
+describe('Readme', function() {
+
+ describe('Parsing', function() {
+ it('should parse empty readme', function() {
+ return mock.setupDefaultBook({
+ 'README.md': ''
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+
+ .then(function() {
+ return book.readme.load();
+ });
+ });
+ });
+
+ it('should parse readme', function() {
+ return mock.setupDefaultBook({
+ 'README.md': '# Hello World\nThis is my book'
+ })
+ .then(function(book) {
+ return book.readme.load()
+ .then(function() {
+ book.readme.title.should.equal('Hello World');
+ book.readme.description.should.equal('This is my book');
+ });
+ });
+ });
+ });
+
+});
+
diff --git a/test/resolve.js b/test/resolve.js
deleted file mode 100644
index e474ed03a..000000000
--- a/test/resolve.js
+++ /dev/null
@@ -1,60 +0,0 @@
-var path = require("path");
-
-describe("Resolve Files", function () {
- var book;
-
- before(function() {
- return books.parse("basic")
- .then(function(_book) {
- book = _book;
- });
- });
-
- describe("book.fileIsInBook", function() {
- it("should return true for correct paths", function() {
- book.fileIsInBook(path.join(book.root, "README.md")).should.equal(true);
- book.fileIsInBook(path.join(book.root, "styles/website.css")).should.equal(true);
- });
-
- it("should return true for root folder", function() {
- book.fileIsInBook(path.join(book.root, "./")).should.equal(true);
- book.fileIsInBook(book.root).should.equal(true);
- });
-
- it("should return false for files out of scope", function() {
- book.fileIsInBook(path.join(book.root, "../")).should.equal(false);
- book.fileIsInBook("README.md").should.equal(false);
- book.fileIsInBook(path.resolve(book.root, "../README.md")).should.equal(false);
- });
-
- it("should correctly handle windows paths", function() {
- book.fileIsInBook(path.join(book.root, "\\styles\\website.css")).should.equal(true);
- });
- });
-
- describe("book.resolve", function() {
- it("should resolve a file to its absolute path", function() {
- book.resolve("README.md").should.equal(path.resolve(book.root, "README.md"));
- book.resolve("website/README.md").should.equal(path.resolve(book.root, "website/README.md"));
- });
-
- it("should correctly handle windows paths", function() {
- book.resolve("styles\\website.css").should.equal(path.resolve(book.root, "styles\\website.css"));
- });
-
- it("should correctly resolve all arguments", function() {
- book.resolve("test", "hello", "..", "README.md").should.equal(path.resolve(book.root, "test/README.md"));
- });
-
- it("should correctly resolve to root folder", function() {
- book.resolve("test", "/README.md").should.equal(path.resolve(book.root, "README.md"));
- book.resolve("test", "\\README.md").should.equal(path.resolve(book.root, "README.md"));
- });
-
- it("should throw an error for file out of book", function() {
- (function() {
- return book.resolve("../README.md");
- }).should.throw();
- });
- });
-});
diff --git a/test/structure.js b/test/structure.js
deleted file mode 100644
index 90413cb4f..000000000
--- a/test/structure.js
+++ /dev/null
@@ -1,20 +0,0 @@
-describe('Structure', function () {
- var book;
-
- before(function() {
- return books.parse('structure')
- .then(function(_book) {
- book = _book;
- });
- });
-
-
- it('should prioritize structure defined in book.json', function() {
- book.readmeFile.should.equal('README.adoc');
- });
-
- it('should be case incensitive', function() {
- book.glossaryFile.should.equal('glossary.md');
- book.glossary.should.have.lengthOf(1);
- });
-});
diff --git a/test/summary.js b/test/summary.js
deleted file mode 100644
index 2d3a2481b..000000000
--- a/test/summary.js
+++ /dev/null
@@ -1,70 +0,0 @@
-var fs = require("fs");
-var path = require("path");
-
-describe("Summary", function () {
- describe("Parsing", function() {
- var book;
-
- before(function() {
- return books.parse("summary")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should correctly list items", function() {
- book.should.have.property("summary");
- book.summary.should.have.property("chapters");
- book.summary.chapters.should.have.lengthOf(4);
- });
-
- it("should correctly mark non-existant entries", function() {
- book.summary.chapters[0].exists.should.have.equal(true);
- book.summary.chapters[1].exists.should.have.equal(true);
- book.summary.chapters[2].exists.should.have.equal(true);
- book.summary.chapters[3].exists.should.have.equal(false);
- });
- });
-
- describe("Generation", function() {
- var book;
-
- before(function() {
- return books.generate("summary", "website")
- .then(function(_book) {
- book = _book;
- });
- });
-
- it("should create files according to summary", function() {
- book.should.have.file("index.html");
- book.should.have.file("PAGE1.html");
- book.should.have.file("folder/PAGE2.html");
- });
-
- it("should correctly output summary", function() {
- var PAGE = fs.readFileSync(
- path.join(book.options.output, "index.html"),
- { encoding: "utf-8" }
- );
-
- PAGE.should.be.html({
- ".book-summary .chapter[data-level=\"0\"] a": {
- attributes: {
- href: "./index.html"
- }
- },
- ".book-summary .chapter[data-level=\"1\"] a": {
- attributes: {
- href: "./PAGE1.html"
- }
- },
- ".book-summary .chapter[data-level=\"2\"] a": {
- attributes: {
- href: "./folder/PAGE2.html"
- }
- }
- });
- });
- });
-});
diff --git a/test/templating.js b/test/templating.js
deleted file mode 100644
index f92154bad..000000000
--- a/test/templating.js
+++ /dev/null
@@ -1,33 +0,0 @@
-var pkg = require("../package.json");
-
-describe("Templating", function () {
- var book;
-
- before(function() {
- return books.parse("basic")
- .then(function(_book) {
- book = _book;
- });
- });
-
- var testTpl = function(str, args, options) {
- return book.template.renderString(str, args, options)
- .then(book.template.postProcess);
- };
-
- describe("Context", function() {
- it("should correctly have access to generator", function() {
- return testTpl("{{ gitbook.generator }}")
- .then(function(content) {
- content.should.equal("website");
- });
- });
-
- it("should correctly have access to gitbook version", function() {
- return testTpl("{{ gitbook.version }}")
- .then(function(content) {
- content.should.equal(pkg.version.toString());
- });
- });
- });
-});
diff --git a/test/website.js b/test/website.js
deleted file mode 100644
index 6a0fd1cb5..000000000
--- a/test/website.js
+++ /dev/null
@@ -1,26 +0,0 @@
-describe('Website generator', function () {
- describe('Basic Book', function() {
- var book;
-
- before(function() {
- return books.generate('basic', 'website')
- .then(function(_book) {
- book = _book;
- });
- });
-
- it('should correctly output an index.html', function() {
- book.should.have.file('index.html');
- });
-
- it('should correctly copy assets', function() {
- book.should.have.file('gitbook');
- book.should.have.file('gitbook/app.js');
- book.should.have.file('gitbook/style.css');
- });
-
- it('should not copy ebook assets', function() {
- book.should.not.have.file('gitbook/ebook.css');
- });
- });
-});
From f305d57ab7702c3ca10fd6e32366d19e524ee1f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 27 Jan 2016 10:24:06 +0100
Subject: [PATCH 003/217] Add more classes structures
---
lib/backbone/article.js | 28 ++++++
lib/backbone/file.js | 2 +-
lib/backbone/page.js | 15 ++++
lib/backbone/summary.js | 1 +
lib/book.js | 57 +++++++++++--
lib/{config_default.js => config/default.js} | 0
lib/{config.js => config/index.js} | 7 +-
lib/generators/base.js | 29 +++++++
lib/generators/index.js | 10 +++
lib/generators/json.js | 14 +++
lib/output.js | 47 ++++++++++
lib/page.js | 10 ---
lib/parsers/asciidoc.js | 27 ++++++
lib/parsers/html.js | 90 ++++++++++++++++++++
lib/parsers/index.js | 11 +++
lib/parsers/markdown.js | 6 ++
lib/plugins/index.js | 0
lib/plugins/manager.js | 10 +++
lib/plugins/plugin.js | 8 ++
lib/templating/blocks.js | 11 +++
lib/templating/index.js | 28 ++++++
lib/templating/loader.js | 0
lib/utils/path.js | 12 ++-
lib/utils/promise.js | 26 ++++++
test/readme.js | 13 +++
25 files changed, 439 insertions(+), 23 deletions(-)
create mode 100644 lib/backbone/article.js
create mode 100644 lib/backbone/page.js
rename lib/{config_default.js => config/default.js} (100%)
rename lib/{config.js => config/index.js} (94%)
create mode 100644 lib/generators/base.js
create mode 100644 lib/generators/index.js
create mode 100644 lib/generators/json.js
create mode 100644 lib/output.js
delete mode 100644 lib/page.js
create mode 100644 lib/parsers/asciidoc.js
create mode 100644 lib/parsers/html.js
create mode 100644 lib/parsers/index.js
create mode 100644 lib/parsers/markdown.js
create mode 100644 lib/plugins/index.js
create mode 100644 lib/plugins/manager.js
create mode 100644 lib/plugins/plugin.js
create mode 100644 lib/templating/blocks.js
create mode 100644 lib/templating/index.js
create mode 100644 lib/templating/loader.js
create mode 100644 lib/utils/promise.js
diff --git a/lib/backbone/article.js b/lib/backbone/article.js
new file mode 100644
index 000000000..68d8236a4
--- /dev/null
+++ b/lib/backbone/article.js
@@ -0,0 +1,28 @@
+var url = require('url');
+var _ = require('lodash');
+
+/*
+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 (optional)
+
+*/
+
+function Article(title, ref, articles) {
+ var parts = url.parse(ref);
+
+ this.title = title;
+ this.filename = parts.pathname;
+ this.anchor = parts.hash;
+ this.articles = _.map(articles || [], function(article) {
+ if (article instanceof Article) return article;
+ return new Article(article.title, article.ref, article.articles);
+ })
+}
+
+// Return true if has children
+Article.prototype.hasChildren = function() {
+ return this.articles.length > 0;
+};
+
+
+module.exports = Article;
diff --git a/lib/backbone/file.js b/lib/backbone/file.js
index 71fc78cf9..34cf06691 100644
--- a/lib/backbone/file.js
+++ b/lib/backbone/file.js
@@ -15,7 +15,7 @@ BackboneFile.prototype.type = '';
// Parse a backbone file
BackboneFile.prototype.parse = function() {
-
+ // To be implemented by each child
};
// Return true if backbone file exists
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
new file mode 100644
index 000000000..fdfe3f7db
--- /dev/null
+++ b/lib/backbone/page.js
@@ -0,0 +1,15 @@
+
+/*
+A page represent a parsable file in the book (Markdown, Asciidoc, etc)
+*/
+
+function Page(book, filename) {
+ if (!(this instanceof Page)) return new Page();
+
+ this.book = book;
+ this.filename = filename;
+}
+
+
+
+module.exports = Page;
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index e2cd48570..7eb6e0c4a 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -1,3 +1,4 @@
+var Article = require('./article');
function Summary() {
if (!(this instanceof Summary)) return new Summary();
diff --git a/lib/book.js b/lib/book.js
index 3bb5bf17f..91dfa0266 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -10,7 +10,7 @@ var Readme = require('./backbone/readme');
var Glossary = require('./backbone/glossary');
var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
-var Page = require('./page');
+var Page = require('./backbone/page');
var pathUtil = require('./utils/path');
function Book(opts) {
@@ -47,7 +47,28 @@ function Book(opts) {
// Rules to ignore some files
this.ignore = Ignore();
- this.ignore.addPattern(['.git', '.svn', '.DS_Store']);
+ this.ignore.addPattern([
+ // Skip Git stuff
+ '.git/',
+ '.gitignore',
+
+ // Skip OS X meta data
+ '.DS_Store',
+
+ // Skip stuff installed by plugins
+ 'node_modules',
+
+ // Skip book outputs
+ '_book',
+ '*.pdf',
+ '*.epub',
+ '*.mobi',
+
+ // Skip config files
+ '.ignore',
+ '.bookignore',
+ 'book.json'
+ ]);
// Create a logger for the book
this.log = new Logger(opts.log, opts.logLevel);
@@ -59,9 +80,12 @@ function Book(opts) {
this.readme = new Readme(this);
this.summary = new Summary(this);
this.glossary = new Glossary(this);
- this.langs = new Langs(this);
- // Map of pages in the bok
+ // Multilinguals book
+ this.langs = new Langs(this);
+ this.books = [];
+
+ // List of page in the book
this.pages = {};
_.bindAll(this);
@@ -110,7 +134,10 @@ Book.prototype.parse = function() {
})
.then(function() {
- if (that.isMultilingual()) return;
+ if (that.isMultilingual()) {
+ that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
+ return;
+ }
return Q()
.then(that.readme.load)
@@ -131,6 +158,26 @@ Book.prototype.parse = function() {
});
};
+// Mark a filename as being parsable
+Book.prototype.addPage = function(filename) {
+ filename = pathUtil.normalize(filename);
+
+ if (this.pages[filename]) return;
+ this.pages[filename] = new Page(this, filename);
+};
+
+// Return a page by its filename (or undefined)
+Book.prototype.getPage = function(filename) {
+ filename = pathUtil.normalize(filename);
+ return this.pages[filename];
+};
+
+
+// Return true, if has a specific page
+Book.prototype.hasPage = function(filename) {
+ return Boolean(this.getPage(filename));
+};
+
// Test if a file is ignored, return true if it is
Book.prototype.isFileIgnored = function(filename) {
return this.ignore.filter([filename]).length == 0;
diff --git a/lib/config_default.js b/lib/config/default.js
similarity index 100%
rename from lib/config_default.js
rename to lib/config/default.js
diff --git a/lib/config.js b/lib/config/index.js
similarity index 94%
rename from lib/config.js
rename to lib/config/index.js
index c6d05dae6..267f65042 100644
--- a/lib/config.js
+++ b/lib/config/index.js
@@ -1,10 +1,9 @@
var Q = require('q');
var _ = require('lodash');
var semver = require('semver');
-var path = require('path');
-var gitbook = require('./gitbook');
-var configDefault = require('./config_default');
+var gitbook = require('../gitbook');
+var configDefault = require('./default');
/*
Config is an interface for the book's configuration stored in "book.json" (or "book.js")
@@ -42,7 +41,7 @@ Config.prototype.load = function() {
}
}
- //that.options.output = path.resolve(that.options.output || that.book.resolve('_book'));
+ that.options.output = that.options.output || that.book.resolve('_book');
//that.options.plugins = normalizePluginsList(that.options.plugins);
//that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false);
//that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
diff --git a/lib/generators/base.js b/lib/generators/base.js
new file mode 100644
index 000000000..554a389de
--- /dev/null
+++ b/lib/generators/base.js
@@ -0,0 +1,29 @@
+
+function Generator(output, type) {
+ this.output = output;
+ this.book = output.book;
+ this.type = type;
+}
+
+// Prepare the generation
+Generator.prototype.prepare = function() {
+
+};
+
+// Copy an asset file (non-parsable), ex: images, etc
+Generator.prototype.writeFile = function(filename) {
+
+};
+
+// Write a page (parsable file), ex: markdown, etc
+Generator.prototype.writePage = function(page) {
+
+};
+
+// Finish the generation
+Generator.prototype.finish = function() {
+
+};
+
+
+module.exports = Generator;
diff --git a/lib/generators/index.js b/lib/generators/index.js
new file mode 100644
index 000000000..de8a1e6dc
--- /dev/null
+++ b/lib/generators/index.js
@@ -0,0 +1,10 @@
+var _ = require('lodash');
+var EbookGenerator = require('./ebook');
+
+module.exports = {
+ json: require('./json'),
+ /*website: require('./website'),
+ pdf: _.partialRight(EbookGenerator, 'pdf'),
+ mobi: _.partialRight(EbookGenerator, 'mobi'),
+ epub: _.partialRight(EbookGenerator, 'epub')*/
+};
diff --git a/lib/generators/json.js b/lib/generators/json.js
new file mode 100644
index 000000000..c5a87fe03
--- /dev/null
+++ b/lib/generators/json.js
@@ -0,0 +1,14 @@
+var util = require('util');
+var Generator = require('./base');
+
+function JSONGenerator() {
+ Generator.apply(this, arguments);
+}
+util.inherits(JSONGenerator, Generator);
+
+
+
+
+
+
+module.exports = JSONGenerator;
diff --git a/lib/output.js b/lib/output.js
new file mode 100644
index 000000000..2f40fb61b
--- /dev/null
+++ b/lib/output.js
@@ -0,0 +1,47 @@
+var Promise = require('utils/promise');
+var generators = require('./generators');
+var PluginsManager = require('./plugins');
+
+function Output(book, type) {
+ if (!generators[type]) throw new Error('Generator not found"' + type + '"');
+
+ this.book = book;
+ this.type = type;
+ this.plugins = new PluginsManager(book);
+ this.generator = new generators[type](this, type);
+}
+
+// Start the generation, for a parsed book
+Output.prototype.generate = function() {
+ var that = this;
+
+ return Promise()
+
+ // Initialize the generation
+ .then(function() {
+ return that.generator.prepare();
+ })
+
+ // Process all files
+ .then(function() {
+ return that.book.fs.listAllFiles(that.book.root);
+ })
+ .then(function(files) {
+ return Promise.serie(files, function(filename) {
+ var isPage = that.book.hasPage(filename);
+
+ if (isPage) {
+ return that.generator.writePage(that.book.getPage(filename));
+ } else {
+ return that.generator.writeFile(filename);
+ }
+ });
+ })
+
+ // Finish the generation
+ .then(function() {
+ return that.generator.finish();
+ });
+};
+
+module.exports = Output;
diff --git a/lib/page.js b/lib/page.js
deleted file mode 100644
index a6c2caf98..000000000
--- a/lib/page.js
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-function Page() {
- if (!(this instanceof Page)) return new Page();
-
-}
-
-
-
-module.exports = Page;
diff --git a/lib/parsers/asciidoc.js b/lib/parsers/asciidoc.js
new file mode 100644
index 000000000..84e619d61
--- /dev/null
+++ b/lib/parsers/asciidoc.js
@@ -0,0 +1,27 @@
+var Asciidoctor = require('asciidoctor.js');
+var htmlParser = require('./html');
+
+var asciidoctor = Asciidoctor();
+var opal = asciidoctor.Opal;
+
+var processor = null;
+var useExtensions = true;
+
+if (useExtensions) {
+ processor = asciidoctor.Asciidoctor(true);
+} else {
+ processor = asciidoctor.Asciidoctor();
+}
+
+
+// Convert asciidoc to HTML
+function asciidocToHTML(content) {
+ var options = opal.hash2(['attributes'], {'attributes': 'showtitle'});
+ return processor.$convert(content, options);
+}
+
+
+module.exports = htmlParser.inherits({
+ extensions: ['.adoc', '.asciidoc'],
+ toHTML: asciidocToHTML
+});
diff --git a/lib/parsers/html.js b/lib/parsers/html.js
new file mode 100644
index 000000000..8f4ed34f7
--- /dev/null
+++ b/lib/parsers/html.js
@@ -0,0 +1,90 @@
+var _ = require('lodash');
+var cheerio = require('cheerio');
+
+// Parse summary and returns a list of sections
+function parseSummary(html) {
+ var sections = [];
+ var $ = cheerio.load(html);
+
+ // Find main container
+ var $body = getContainer($);
+
+ // Extract sections, and parse
+ var $lists = $body.find('> ul, > ol');
+
+ $lists.each(function() {
+ sections.push({
+ articles: parseList($(this), $)
+ });
+ });
+
+ return sections;
+}
+
+// Parse readme and extract title, description
+function parseReadme(html) {
+ var $ = cheerio.load(html);
+
+ // Find main container
+ var $body = getContainer($);
+
+ return {
+ title: $body.find('h1:first-child').text().trim(),
+ description: $body.find('div.paragraph').first().text().trim()
+ };
+}
+
+// Return a page container (html, body tag or directly the root element)
+function getContainer($) {
+ var $body = $('body, html').first();
+ if (!$body) $body = $;
+
+ return $body;
+}
+
+// Parse a ul list and return list of chapters recursvely
+function parseList($ul, $) {
+ var articles = [];
+
+ $ul.children('li').each(function() {
+ var article = {};
+
+ var $li = $(this);
+
+ var $text = $li.find('> p, > span');
+ var $a = $li.find('> a, > p a, > span a');
+
+ article.title = $text.text();
+ if ($a.length > 0) {
+ article.title = $a.first().text();
+ article.ref = $a.attr('href');
+ }
+
+ // Inner list, with children article
+ var $sub = $li.find('> ol, > ul, > .olist > ol');
+ article.articles = parseList($sub, $);
+
+ articles.push(article);
+ });
+
+ return articles;
+}
+
+
+// Inherit from the html parser
+function inherits(opts) {
+ var parser = _.defaults(opts, {
+ toHTML: _.identity
+ });
+
+ parser.readme = _.compose(opts.toHTML, parseReadme);
+ parser.summary = _.compose(opts.toHTML, parseSummary);
+
+ return parser;
+}
+
+
+module.exports = inherits({
+ extensions: ['.html']
+});
+module.exports.inherits = inherits;
diff --git a/lib/parsers/index.js b/lib/parsers/index.js
new file mode 100644
index 000000000..d65038663
--- /dev/null
+++ b/lib/parsers/index.js
@@ -0,0 +1,11 @@
+
+var PARSERS = {
+ html: require('./html'),
+ markdown: require('./markdown'),
+ asciidoc: require('./asciidoc')
+};
+
+
+module.exports = {
+
+};
diff --git a/lib/parsers/markdown.js b/lib/parsers/markdown.js
new file mode 100644
index 000000000..aac285879
--- /dev/null
+++ b/lib/parsers/markdown.js
@@ -0,0 +1,6 @@
+
+
+
+module.exports = {
+ extensions: ['.md', '.markdown', '.mdown']
+};
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/plugins/manager.js b/lib/plugins/manager.js
new file mode 100644
index 000000000..8617044cf
--- /dev/null
+++ b/lib/plugins/manager.js
@@ -0,0 +1,10 @@
+
+var BookPlugin = require('./plugin');
+
+function PluginsManager(book) {
+ this.book = book;
+ this.plugins = [];
+}
+
+
+module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
new file mode 100644
index 000000000..8397205c7
--- /dev/null
+++ b/lib/plugins/plugin.js
@@ -0,0 +1,8 @@
+
+function BookPlugin() {
+
+
+}
+
+
+module.exports = BookPlugin;
diff --git a/lib/templating/blocks.js b/lib/templating/blocks.js
new file mode 100644
index 000000000..92097a707
--- /dev/null
+++ b/lib/templating/blocks.js
@@ -0,0 +1,11 @@
+var _ = require('lodash');
+
+module.exports = {
+ // Return non-parsed html
+ // since blocks are by default non-parsable, a simple identity method works fine
+ html: _.identity,
+
+ // Highlight a code block
+ // This block can be extent by plugins
+ code: _.identity
+};
diff --git a/lib/templating/index.js b/lib/templating/index.js
new file mode 100644
index 000000000..1031bdbfe
--- /dev/null
+++ b/lib/templating/index.js
@@ -0,0 +1,28 @@
+var nunjucks = require('nunjucks');
+
+function TemplatingEngine(book) {
+ this.book = book;
+ this.log = book.log;
+
+
+ this.nunjucks = new nunjucks.Environment(
+ this.loader,
+ {
+ // Escaping is done after by the asciidoc/markdown parser
+ autoescape: false,
+
+ // Syntax
+ tags: {
+ blockStart: '{%',
+ blockEnd: '%}',
+ variableStart: '{{',
+ variableEnd: '}}',
+ commentStart: '{###',
+ commentEnd: '###}'
+ }
+ }
+ );
+}
+
+
+module.exports = TemplatingEngine;
diff --git a/lib/templating/loader.js b/lib/templating/loader.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/utils/path.js b/lib/utils/path.js
index dc97d5da3..2d722a542 100644
--- a/lib/utils/path.js
+++ b/lib/utils/path.js
@@ -1,6 +1,11 @@
var _ = require('lodash');
var path = require('path');
+// Normalize a filename
+function normalizePath(filename) {
+ return path.normalize(filename);
+}
+
// Return true if file path is inside a folder
function isInRoot(root, filename) {
filename = path.normalize(filename);
@@ -17,10 +22,10 @@ function resolveInRoot(root) {
.slice(1)
.reduce(function(current, p) {
// Handle path relative to book root ("/README.md")
- if (p[0] == "/" || p[0] == "\\") return p.slice(1);
+ if (p[0] == '/' || p[0] == '\\') return p.slice(1);
return current? path.join(current, p) : path.normalize(p);
- }, "")
+ }, '')
.value();
result = path.resolve(root, input);
@@ -36,5 +41,6 @@ function resolveInRoot(root) {
module.exports = {
isInRoot: isInRoot,
- resolveInRoot: resolveInRoot
+ resolveInRoot: resolveInRoot,
+ normalize: normalizePath
};
diff --git a/lib/utils/promise.js b/lib/utils/promise.js
new file mode 100644
index 000000000..c25b34935
--- /dev/null
+++ b/lib/utils/promise.js
@@ -0,0 +1,26 @@
+var Q = require('q');
+var _ = require('lodash');
+
+// Reduce an array to a promise
+function reduce(arr, iter, base) {
+ return _.reduce(arr, function(prev, elem, i) {
+ return prev.then(function(val) {
+ return iter(val, elem, i);
+ });
+ }, Q(base));
+}
+
+// Transform an array
+function serie(arr, iter, base) {
+ return reduce(arr, function(before, item, i) {
+ return Q(iter(item, i))
+ .then(function(r) {
+ before.push(r);
+ return before;
+ });
+ }, []);
+}
+
+module.exports = Q;
+module.exports.reduce = reduce;
+module.exports.serie = serie;
diff --git a/test/readme.js b/test/readme.js
index 0ebb4903f..fd084cb4b 100644
--- a/test/readme.js
+++ b/test/readme.js
@@ -28,6 +28,19 @@ describe('Readme', function() {
});
});
});
+
+ it('should parse AsciiDoc readme', function() {
+ return mock.setupBook({
+ 'README.adoc': '# Hello World\n\nThis is my book\n'
+ })
+ .then(function(book) {
+ return book.readme.load()
+ .then(function() {
+ book.readme.title.should.equal('Hello World');
+ book.readme.description.should.equal('This is my book');
+ });
+ });
+ });
});
});
From 8d277e9108afa6a027c61feb581a2150958f8571 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 27 Jan 2016 21:56:00 +0100
Subject: [PATCH 004/217] Add parsing of the glossary
---
lib/backbone/article.js | 2 +-
lib/backbone/glossary.js | 51 ++++++++++++++++++++++-
lib/backbone/page.js | 4 ++
lib/generators/base.js | 2 +-
lib/generators/json.js | 5 +++
lib/index.js | 1 +
lib/output.js | 7 +++-
lib/parsers/asciidoc.js | 27 ------------
lib/parsers/html.js | 90 ----------------------------------------
lib/parsers/index.js | 11 -----
lib/parsers/markdown.js | 6 ---
11 files changed, 68 insertions(+), 138 deletions(-)
delete mode 100644 lib/parsers/asciidoc.js
delete mode 100644 lib/parsers/html.js
delete mode 100644 lib/parsers/index.js
delete mode 100644 lib/parsers/markdown.js
diff --git a/lib/backbone/article.js b/lib/backbone/article.js
index 68d8236a4..299df25b9 100644
--- a/lib/backbone/article.js
+++ b/lib/backbone/article.js
@@ -16,7 +16,7 @@ function Article(title, ref, articles) {
this.articles = _.map(articles || [], function(article) {
if (article instanceof Article) return article;
return new Article(article.title, article.ref, article.articles);
- })
+ });
}
// Return true if has children
diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js
index aba83cf3c..b25d7c72a 100644
--- a/lib/backbone/glossary.js
+++ b/lib/backbone/glossary.js
@@ -1,8 +1,57 @@
+var _ = require('lodash');
+var util = require('util');
+var BackboneFile = require('./file');
+
+/*
+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 Glossary)) return new Glossary();
+
+ 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 this.name.toLowerCase()
+ .replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
+ .replace(/ /g, '_')
+ .trim();
+};
+
function Glossary() {
- if (!(this instanceof Glossary)) return new Glossary();
+ BackboneFile.apply(this, arguments);
+
+ this.entries = [];
}
+util.inherits(Glossary, BackboneFile);
Glossary.prototype.type = 'glossary';
+// 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
+ });
+};
+
module.exports = Glossary;
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index fdfe3f7db..33bd63687 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -10,6 +10,10 @@ function Page(book, filename) {
this.filename = filename;
}
+// Return the filename of the page with another extension
+Page.prototype.withExtension = function(ext) {
+ return
+};
module.exports = Page;
diff --git a/lib/generators/base.js b/lib/generators/base.js
index 554a389de..0fc0be91a 100644
--- a/lib/generators/base.js
+++ b/lib/generators/base.js
@@ -11,7 +11,7 @@ Generator.prototype.prepare = function() {
};
// Copy an asset file (non-parsable), ex: images, etc
-Generator.prototype.writeFile = function(filename) {
+Generator.prototype.writeAsset = function(filename) {
};
diff --git a/lib/generators/json.js b/lib/generators/json.js
index c5a87fe03..820574b49 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -6,7 +6,12 @@ function JSONGenerator() {
}
util.inherits(JSONGenerator, Generator);
+// Write a page (parsable file)
+Generator.prototype.writePage = function(page) {
+ return this.output.writeFile(page.withExtension('.json'));
+
+};
diff --git a/lib/index.js b/lib/index.js
index 13a572da3..59346f2de 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,4 +1,5 @@
var Book = require('./book');
+var Output = require('./output');
module.exports = {
Book: Book
diff --git a/lib/output.js b/lib/output.js
index 2f40fb61b..1c694a13a 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -11,6 +11,11 @@ function Output(book, type) {
this.generator = new generators[type](this, type);
}
+// Write a file to the output folder
+Output.prototype.writeFile = function(filename, buf) {
+
+};
+
// Start the generation, for a parsed book
Output.prototype.generate = function() {
var that = this;
@@ -33,7 +38,7 @@ Output.prototype.generate = function() {
if (isPage) {
return that.generator.writePage(that.book.getPage(filename));
} else {
- return that.generator.writeFile(filename);
+ return that.generator.writeAsset(filename);
}
});
})
diff --git a/lib/parsers/asciidoc.js b/lib/parsers/asciidoc.js
deleted file mode 100644
index 84e619d61..000000000
--- a/lib/parsers/asciidoc.js
+++ /dev/null
@@ -1,27 +0,0 @@
-var Asciidoctor = require('asciidoctor.js');
-var htmlParser = require('./html');
-
-var asciidoctor = Asciidoctor();
-var opal = asciidoctor.Opal;
-
-var processor = null;
-var useExtensions = true;
-
-if (useExtensions) {
- processor = asciidoctor.Asciidoctor(true);
-} else {
- processor = asciidoctor.Asciidoctor();
-}
-
-
-// Convert asciidoc to HTML
-function asciidocToHTML(content) {
- var options = opal.hash2(['attributes'], {'attributes': 'showtitle'});
- return processor.$convert(content, options);
-}
-
-
-module.exports = htmlParser.inherits({
- extensions: ['.adoc', '.asciidoc'],
- toHTML: asciidocToHTML
-});
diff --git a/lib/parsers/html.js b/lib/parsers/html.js
deleted file mode 100644
index 8f4ed34f7..000000000
--- a/lib/parsers/html.js
+++ /dev/null
@@ -1,90 +0,0 @@
-var _ = require('lodash');
-var cheerio = require('cheerio');
-
-// Parse summary and returns a list of sections
-function parseSummary(html) {
- var sections = [];
- var $ = cheerio.load(html);
-
- // Find main container
- var $body = getContainer($);
-
- // Extract sections, and parse
- var $lists = $body.find('> ul, > ol');
-
- $lists.each(function() {
- sections.push({
- articles: parseList($(this), $)
- });
- });
-
- return sections;
-}
-
-// Parse readme and extract title, description
-function parseReadme(html) {
- var $ = cheerio.load(html);
-
- // Find main container
- var $body = getContainer($);
-
- return {
- title: $body.find('h1:first-child').text().trim(),
- description: $body.find('div.paragraph').first().text().trim()
- };
-}
-
-// Return a page container (html, body tag or directly the root element)
-function getContainer($) {
- var $body = $('body, html').first();
- if (!$body) $body = $;
-
- return $body;
-}
-
-// Parse a ul list and return list of chapters recursvely
-function parseList($ul, $) {
- var articles = [];
-
- $ul.children('li').each(function() {
- var article = {};
-
- var $li = $(this);
-
- var $text = $li.find('> p, > span');
- var $a = $li.find('> a, > p a, > span a');
-
- article.title = $text.text();
- if ($a.length > 0) {
- article.title = $a.first().text();
- article.ref = $a.attr('href');
- }
-
- // Inner list, with children article
- var $sub = $li.find('> ol, > ul, > .olist > ol');
- article.articles = parseList($sub, $);
-
- articles.push(article);
- });
-
- return articles;
-}
-
-
-// Inherit from the html parser
-function inherits(opts) {
- var parser = _.defaults(opts, {
- toHTML: _.identity
- });
-
- parser.readme = _.compose(opts.toHTML, parseReadme);
- parser.summary = _.compose(opts.toHTML, parseSummary);
-
- return parser;
-}
-
-
-module.exports = inherits({
- extensions: ['.html']
-});
-module.exports.inherits = inherits;
diff --git a/lib/parsers/index.js b/lib/parsers/index.js
deleted file mode 100644
index d65038663..000000000
--- a/lib/parsers/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-
-var PARSERS = {
- html: require('./html'),
- markdown: require('./markdown'),
- asciidoc: require('./asciidoc')
-};
-
-
-module.exports = {
-
-};
diff --git a/lib/parsers/markdown.js b/lib/parsers/markdown.js
deleted file mode 100644
index aac285879..000000000
--- a/lib/parsers/markdown.js
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-module.exports = {
- extensions: ['.md', '.markdown', '.mdown']
-};
From ddc99104d4394cc21eaa615b01d73ec001ac7a16 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 27 Jan 2016 22:48:23 +0100
Subject: [PATCH 005/217] Add unit tests for glossary parsing
---
lib/backbone/glossary.js | 7 +++-
lib/backbone/summary.js | 8 +++-
lib/book.js | 20 +++++-----
lib/generators/index.js | 2 +-
lib/output.js | 2 +-
test/glossary.js | 80 ++++++++++++++++++++++++++++++++++++++++
6 files changed, 104 insertions(+), 15 deletions(-)
create mode 100644 test/glossary.js
diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js
index b25d7c72a..0f5e567d1 100644
--- a/lib/backbone/glossary.js
+++ b/lib/backbone/glossary.js
@@ -7,7 +7,7 @@ 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 Glossary)) return new Glossary();
+ if (!(this instanceof GlossaryEntry)) return new GlossaryEntry(name, description);
this.name = name;
this.description = description;
@@ -54,4 +54,9 @@ Glossary.prototype.get = function(id) {
});
};
+// Return false if glossary has entries (and exists)
+Glossary.prototype.isEmpty = function(id) {
+ return _.size(this.entries) === 0;
+};
+
module.exports = Glossary;
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 7eb6e0c4a..96815a6d3 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -1,8 +1,14 @@
+var util = require('util');
+var BackboneFile = require('./file');
+
var Article = require('./article');
function Summary() {
- if (!(this instanceof Summary)) return new Summary();
+ BackboneFile.apply(this, arguments);
+
+ this.articles = [];
}
+util.inherits(Summary, BackboneFile);
Summary.prototype.type = 'summary';
diff --git a/lib/book.js b/lib/book.js
index 91dfa0266..77539ce5c 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -1,5 +1,4 @@
var _ = require('lodash');
-var Q = require('q');
var path = require('path');
var Ignore = require('ignore');
var parsers = require('gitbook-parsers');
@@ -12,6 +11,7 @@ var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
var Page = require('./backbone/page');
var pathUtil = require('./utils/path');
+var Promise = require('./utils/promise');
function Book(opts) {
if (!(this instanceof Book)) return new Book(opts);
@@ -106,25 +106,23 @@ Book.prototype.resolve = function() {
Book.prototype.parseIgnoreRules = function() {
var that = this;
- return _.reduce([
+ return Promise.series([
'.ignore',
'.gitignore',
'.bookignore'
- ], function(prev, filename) {
- return prev.then(function() {
- return that.readFile(filename);
- })
+ ], function(filename) {
+ return that.readFile(filename)
.then(function(content) {
that.ignore.addPattern(content.toString().split(/\r?\n/));
});
- }, Q());
+ });
};
// Parse the whole book
Book.prototype.parse = function() {
var that = this;
- return Q()
+ return Promise()
.then(this.prepareConfig)
.then(this.parseIgnoreRules)
@@ -139,7 +137,7 @@ Book.prototype.parse = function() {
return;
}
- return Q()
+ return Promise()
.then(that.readme.load)
.then(function() {
if (that.readme.exists()) return;
@@ -185,7 +183,7 @@ Book.prototype.isFileIgnored = function(filename) {
// Read a file in the book, throw error if ignored
Book.prototype.readFile = function(filename) {
- if (this.isFileIgnored(filename)) return Q.reject(new Error('File "'+filename+'" is ignored'));
+ if (this.isFileIgnored(filename)) return Promise.reject(new Error('File "'+filename+'" is ignored'));
return this.fs.readAsString(this.resolve(filename));
};
@@ -217,7 +215,7 @@ Book.prototype.findParsableFile = function(filename) {
};
});
});
- }, Q(null));
+ }, Promise(null));
};
// Return true if book is associated to a language
diff --git a/lib/generators/index.js b/lib/generators/index.js
index de8a1e6dc..dcb2ffe1a 100644
--- a/lib/generators/index.js
+++ b/lib/generators/index.js
@@ -1,5 +1,5 @@
var _ = require('lodash');
-var EbookGenerator = require('./ebook');
+//var EbookGenerator = require('./ebook');
module.exports = {
json: require('./json'),
diff --git a/lib/output.js b/lib/output.js
index 1c694a13a..2e5bc6583 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1,4 +1,4 @@
-var Promise = require('utils/promise');
+var Promise = require('./utils/promise');
var generators = require('./generators');
var PluginsManager = require('./plugins');
diff --git a/test/glossary.js b/test/glossary.js
new file mode 100644
index 000000000..e2c9e2bb6
--- /dev/null
+++ b/test/glossary.js
@@ -0,0 +1,80 @@
+var should = require('should');
+var mock = require('./mock');
+
+describe('Glossary', function() {
+
+ describe('Parsing', function() {
+ it('should parse empty glossary', function() {
+ return mock.setupDefaultBook({
+ 'GLOSSARY.md': ''
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+
+ .then(function() {
+ return book.glossary.load();
+ })
+ .then(function() {
+ book.glossary.isEmpty().should.be.true();
+ });
+ });
+ });
+
+ describe('Non-empty glossary', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'GLOSSARY.md': '# Glossary\n\n### Hello World\n\nThis is an entry'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.prepareConfig();
+ })
+ .then(function() {
+ return book.glossary.load();
+ });
+ });
+
+ it('should not be empty', function() {
+ book.glossary.isEmpty().should.be.false();
+ });
+
+ describe('glossary.get', function() {
+ it('should return an existing entry', function() {
+ var entry = book.glossary.get('hello_world');
+ should.exist(entry);
+
+ entry.name.should.equal('Hello World');
+ entry.description.should.equal('This is an entry');
+ entry.id.should.equal('hello_world');
+ });
+
+ it('should undefined return non existing entry', function() {
+ var entry = book.glossary.get('cool');
+ should.not.exist(entry);
+ });
+ });
+ });
+
+ it('should parse glossary entries', function() {
+ return mock.setupDefaultBook({
+ 'GLOSSARY.md': '# Glossary\n\n### Hello World\n\nThis is an entry'
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+
+ .then(function() {
+ return book.glossary.load();
+ })
+ .then(function() {
+
+
+
+ });
+ });
+ });
+ });
+
+});
+
From 36f2f5d52e54c03036f05225df3f58e9833f3e71 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 27 Jan 2016 22:51:07 +0100
Subject: [PATCH 006/217] Add method glossary.find
---
lib/backbone/glossary.js | 19 +++++++++++++++----
test/glossary.js | 23 +++++++++--------------
2 files changed, 24 insertions(+), 18 deletions(-)
diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js
index 0f5e567d1..22c4d4290 100644
--- a/lib/backbone/glossary.js
+++ b/lib/backbone/glossary.js
@@ -2,6 +2,15 @@ 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
@@ -19,10 +28,7 @@ function GlossaryEntry(name, description) {
// Normalizes a glossary entry's name to create an ID
GlossaryEntry.prototype.getId = function() {
- return this.name.toLowerCase()
- .replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
- .replace(/ /g, '_')
- .trim();
+ return nameToId(this.name);
};
@@ -54,6 +60,11 @@ Glossary.prototype.get = function(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;
diff --git a/test/glossary.js b/test/glossary.js
index e2c9e2bb6..116eabf6f 100644
--- a/test/glossary.js
+++ b/test/glossary.js
@@ -55,22 +55,17 @@ describe('Glossary', function() {
should.not.exist(entry);
});
});
- });
-
- it('should parse glossary entries', function() {
- return mock.setupDefaultBook({
- 'GLOSSARY.md': '# Glossary\n\n### Hello World\n\nThis is an entry'
- })
- .then(function(book) {
- return book.prepareConfig()
-
- .then(function() {
- return book.glossary.load();
- })
- .then(function() {
-
+ describe('glossary.find', function() {
+ it('should return an existing entry', function() {
+ var entry = book.glossary.find('HeLLo World');
+ should.exist(entry);
+ entry.id.should.equal('hello_world');
+ });
+ it('should undefined return non existing entry', function() {
+ var entry = book.glossary.find('Hello');
+ should.not.exist(entry);
});
});
});
From a063cc1a53bede0a869b184f5ec4793ce300ae01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 16:27:29 +0100
Subject: [PATCH 007/217] Add base for langs parsing
---
lib/backbone/glossary.js | 3 +++
lib/backbone/langs.js | 54 ++++++++++++++++++++++++++++++++++++++--
lib/book.js | 51 ++++++++++++++++++++++++++++++++++---
lib/output.js | 7 ++++--
4 files changed, 108 insertions(+), 7 deletions(-)
diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js
index 22c4d4290..9aed1ac35 100644
--- a/lib/backbone/glossary.js
+++ b/lib/backbone/glossary.js
@@ -32,6 +32,9 @@ GlossaryEntry.prototype.getId = function() {
};
+/*
+A glossary is a list of entries stored in a GLOSSARY.md file
+*/
function Glossary() {
BackboneFile.apply(this, arguments);
diff --git a/lib/backbone/langs.js b/lib/backbone/langs.js
index 2818519ef..a40bd8090 100644
--- a/lib/backbone/langs.js
+++ b/lib/backbone/langs.js
@@ -1,15 +1,65 @@
+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() {
- if (!(this instanceof Langs)) return new 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 this.languages.length;
+ return _.size(this.languages);
};
module.exports = Langs;
diff --git a/lib/book.js b/lib/book.js
index 77539ce5c..73a43ec47 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -97,9 +97,27 @@ Book.prototype.prepareConfig = function() {
};
// Resolve a path in the book source
-// Enforce that the output path in the root folder
+// Enforce that the output path is in the scope
Book.prototype.resolve = function() {
- return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments)));
+ var filename = path.resolve.apply([this.root].concat(_.toArray(arguments)));
+ if (!this.isFileInScope(filename)) {
+ var err = new Error('EACCESS: "' + filename + '" not in "' + this.root + '"');
+ err.code = 'EACCESS';
+ throw err;
+ }
+
+ return filename;
+};
+
+// Return false if a file is outside the book' scope
+Book.prototype.isFileInScope = function(filename) {
+ filename = path.resolve(this.root, filename);
+
+ // Is the file in the scope of the parent?
+ if (this.parent && this.parent.isFileInScope(filename)) return true;
+
+ // Is file in the root folder?
+ return pathUtil.isInRoot(this.root, filename);
};
// Parse .gitignore, etc to extract rules
@@ -133,8 +151,25 @@ Book.prototype.parse = function() {
.then(function() {
if (that.isMultilingual()) {
+ if (that.isLanguageBook()) {
+ throw new Error('A multilingual book as a language book is forbidden');
+ }
+
that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
- return;
+
+ // Create a new book for each language and parse it
+ return Promise.serial(that.langs.list(), function(lang) {
+ that.log.debug.ln('Preparing book for language', lang.id);
+ var langBook = new Book({
+ fs: that.fs,
+ parent: that,
+ root: that.resolve(lang.id)
+ });
+
+ that.books.push(langBook);
+
+ return langBook.parse();
+ });
}
return Promise()
@@ -229,4 +264,14 @@ Book.prototype.isMultilingual = function() {
return this.langs.count() > 0;
};
+// Return true if file is in the scope of a child book
+Book.prototype.isInLanguageBook = function(filename) {
+ return _.some(this.langs.list(), function(lang) {
+ return pathUtil.isInRoot(
+ this.resolve(lang.id),
+ this.resolve(filename)
+ );
+ });
+};
+
module.exports = Book;
diff --git a/lib/output.js b/lib/output.js
index 2e5bc6583..d09f29de9 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -19,6 +19,7 @@ Output.prototype.writeFile = function(filename, buf) {
// Start the generation, for a parsed book
Output.prototype.generate = function() {
var that = this;
+ var isMultilingual = this.isMultilingual();
return Promise()
@@ -33,9 +34,11 @@ Output.prototype.generate = function() {
})
.then(function(files) {
return Promise.serie(files, function(filename) {
- var isPage = that.book.hasPage(filename);
+ // Ignore file present in a language book
+ if (isMultilingual && that.book.isInLanguageBook(filename)) return;
- if (isPage) {
+ // Process file as page or asset
+ if (that.book.hasPage(filename)) {
return that.generator.writePage(that.book.getPage(filename));
} else {
return that.generator.writeAsset(filename);
From 46efdacd6d3d5c7560d8f956e2339c36157f7c89 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 16:29:09 +0100
Subject: [PATCH 008/217] Fix book.resolve
---
lib/book.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/book.js b/lib/book.js
index 73a43ec47..c211adff4 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -99,7 +99,7 @@ Book.prototype.prepareConfig = function() {
// Resolve a path in the book source
// Enforce that the output path is in the scope
Book.prototype.resolve = function() {
- var filename = path.resolve.apply([this.root].concat(_.toArray(arguments)));
+ var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
if (!this.isFileInScope(filename)) {
var err = new Error('EACCESS: "' + filename + '" not in "' + this.root + '"');
err.code = 'EACCESS';
From 461ef2c68e9086bd77f0a25d57956ebf60308f7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 17:35:32 +0100
Subject: [PATCH 009/217] Add unit tests for langs
---
test/{config.js => 1-config.js} | 0
test/2-readme.js | 43 +++++++++++++++++++
test/3-glossary.js | 71 +++++++++++++++++++++++++++++++
test/4-langs.js | 35 +++++++++++++++
test/{parse.js => 6-parse.js} | 2 -
test/glossary.js | 75 ---------------------------------
test/readme.js | 47 ---------------------
7 files changed, 149 insertions(+), 124 deletions(-)
rename test/{config.js => 1-config.js} (100%)
create mode 100644 test/2-readme.js
create mode 100644 test/3-glossary.js
create mode 100644 test/4-langs.js
rename test/{parse.js => 6-parse.js} (99%)
delete mode 100644 test/glossary.js
delete mode 100644 test/readme.js
diff --git a/test/config.js b/test/1-config.js
similarity index 100%
rename from test/config.js
rename to test/1-config.js
diff --git a/test/2-readme.js b/test/2-readme.js
new file mode 100644
index 000000000..16aa81ae2
--- /dev/null
+++ b/test/2-readme.js
@@ -0,0 +1,43 @@
+var mock = require('./mock');
+
+describe('Readme', function() {
+ it('should parse empty readme', function() {
+ return mock.setupDefaultBook({
+ 'README.md': ''
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+
+ .then(function() {
+ return book.readme.load();
+ });
+ });
+ });
+
+ it('should parse readme', function() {
+ return mock.setupDefaultBook({
+ 'README.md': '# Hello World\nThis is my book'
+ })
+ .then(function(book) {
+ return book.readme.load()
+ .then(function() {
+ book.readme.title.should.equal('Hello World');
+ book.readme.description.should.equal('This is my book');
+ });
+ });
+ });
+
+ it('should parse AsciiDoc readme', function() {
+ return mock.setupBook({
+ 'README.adoc': '# Hello World\n\nThis is my book\n'
+ })
+ .then(function(book) {
+ return book.readme.load()
+ .then(function() {
+ book.readme.title.should.equal('Hello World');
+ book.readme.description.should.equal('This is my book');
+ });
+ });
+ });
+});
+
diff --git a/test/3-glossary.js b/test/3-glossary.js
new file mode 100644
index 000000000..176298ff5
--- /dev/null
+++ b/test/3-glossary.js
@@ -0,0 +1,71 @@
+var should = require('should');
+var mock = require('./mock');
+
+describe('Glossary', function() {
+ it('should parse empty glossary', function() {
+ return mock.setupDefaultBook({
+ 'GLOSSARY.md': ''
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+
+ .then(function() {
+ return book.glossary.load();
+ })
+ .then(function() {
+ book.glossary.isEmpty().should.be.true();
+ });
+ });
+ });
+
+ describe('Non-empty glossary', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'GLOSSARY.md': '# Glossary\n\n### Hello World\n\nThis is an entry'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.prepareConfig();
+ })
+ .then(function() {
+ return book.glossary.load();
+ });
+ });
+
+ it('should not be empty', function() {
+ book.glossary.isEmpty().should.be.false();
+ });
+
+ describe('glossary.get', function() {
+ it('should return an existing entry', function() {
+ var entry = book.glossary.get('hello_world');
+ should.exist(entry);
+
+ entry.name.should.equal('Hello World');
+ entry.description.should.equal('This is an entry');
+ entry.id.should.equal('hello_world');
+ });
+
+ it('should undefined return non existing entry', function() {
+ var entry = book.glossary.get('cool');
+ should.not.exist(entry);
+ });
+ });
+
+ describe('glossary.find', function() {
+ it('should return an existing entry', function() {
+ var entry = book.glossary.find('HeLLo World');
+ should.exist(entry);
+ entry.id.should.equal('hello_world');
+ });
+
+ it('should undefined return non existing entry', function() {
+ var entry = book.glossary.find('Hello');
+ should.not.exist(entry);
+ });
+ });
+ });
+});
+
diff --git a/test/4-langs.js b/test/4-langs.js
new file mode 100644
index 000000000..599cb4364
--- /dev/null
+++ b/test/4-langs.js
@@ -0,0 +1,35 @@
+var mock = require('./mock');
+
+describe('Langs', function() {
+ it('should parse empty langs', function() {
+ return mock.setupDefaultBook({
+ 'LANGS.md': ''
+ })
+ .then(function(book) {
+ return book.prepareConfig()
+
+ .then(function() {
+ return book.langs.load();
+ })
+
+ .then(function() {
+ book.langs.count().should.equal(0);
+ });
+ });
+ });
+
+ it('should parse languages list', function() {
+ return mock.setupDefaultBook({
+ 'LANGS.md': '# Languages\n\n'
+ + '* [en](./en)\n'
+ + '* [fr](./fr)\n\n'
+ })
+ .then(function(book) {
+ return book.langs.load()
+ .then(function() {
+ book.langs.count().should.equal(2);
+ });
+ });
+ });
+});
+
diff --git a/test/parse.js b/test/6-parse.js
similarity index 99%
rename from test/parse.js
rename to test/6-parse.js
index dca780e03..422744d3f 100644
--- a/test/parse.js
+++ b/test/6-parse.js
@@ -1,7 +1,6 @@
var mock = require('./mock');
describe('Parsing', function() {
-
it('should fail without SUMMARY', function() {
return mock.setupBook({
'README.md': ''
@@ -19,6 +18,5 @@ describe('Parsing', function() {
return book.parse().should.be.rejected;
});
});
-
});
diff --git a/test/glossary.js b/test/glossary.js
deleted file mode 100644
index 116eabf6f..000000000
--- a/test/glossary.js
+++ /dev/null
@@ -1,75 +0,0 @@
-var should = require('should');
-var mock = require('./mock');
-
-describe('Glossary', function() {
-
- describe('Parsing', function() {
- it('should parse empty glossary', function() {
- return mock.setupDefaultBook({
- 'GLOSSARY.md': ''
- })
- .then(function(book) {
- return book.prepareConfig()
-
- .then(function() {
- return book.glossary.load();
- })
- .then(function() {
- book.glossary.isEmpty().should.be.true();
- });
- });
- });
-
- describe('Non-empty glossary', function() {
- var book;
-
- before(function() {
- return mock.setupDefaultBook({
- 'GLOSSARY.md': '# Glossary\n\n### Hello World\n\nThis is an entry'
- })
- .then(function(_book) {
- book = _book;
- return book.prepareConfig();
- })
- .then(function() {
- return book.glossary.load();
- });
- });
-
- it('should not be empty', function() {
- book.glossary.isEmpty().should.be.false();
- });
-
- describe('glossary.get', function() {
- it('should return an existing entry', function() {
- var entry = book.glossary.get('hello_world');
- should.exist(entry);
-
- entry.name.should.equal('Hello World');
- entry.description.should.equal('This is an entry');
- entry.id.should.equal('hello_world');
- });
-
- it('should undefined return non existing entry', function() {
- var entry = book.glossary.get('cool');
- should.not.exist(entry);
- });
- });
-
- describe('glossary.find', function() {
- it('should return an existing entry', function() {
- var entry = book.glossary.find('HeLLo World');
- should.exist(entry);
- entry.id.should.equal('hello_world');
- });
-
- it('should undefined return non existing entry', function() {
- var entry = book.glossary.find('Hello');
- should.not.exist(entry);
- });
- });
- });
- });
-
-});
-
diff --git a/test/readme.js b/test/readme.js
deleted file mode 100644
index fd084cb4b..000000000
--- a/test/readme.js
+++ /dev/null
@@ -1,47 +0,0 @@
-var mock = require('./mock');
-
-describe('Readme', function() {
-
- describe('Parsing', function() {
- it('should parse empty readme', function() {
- return mock.setupDefaultBook({
- 'README.md': ''
- })
- .then(function(book) {
- return book.prepareConfig()
-
- .then(function() {
- return book.readme.load();
- });
- });
- });
-
- it('should parse readme', function() {
- return mock.setupDefaultBook({
- 'README.md': '# Hello World\nThis is my book'
- })
- .then(function(book) {
- return book.readme.load()
- .then(function() {
- book.readme.title.should.equal('Hello World');
- book.readme.description.should.equal('This is my book');
- });
- });
- });
-
- it('should parse AsciiDoc readme', function() {
- return mock.setupBook({
- 'README.adoc': '# Hello World\n\nThis is my book\n'
- })
- .then(function(book) {
- return book.readme.load()
- .then(function() {
- book.readme.title.should.equal('Hello World');
- book.readme.description.should.equal('This is my book');
- });
- });
- });
- });
-
-});
-
From 4b465bedea94688a9adfb9136c9d2970d5496274 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 18:39:44 +0100
Subject: [PATCH 010/217] Fix tests for languages parsing
---
lib/backbone/file.js | 3 ++
lib/backbone/page.js | 7 ++++-
lib/book.js | 14 ++++-----
lib/config/index.js | 30 +++++++++++++++----
lib/fs/index.js | 7 +++--
lib/fs/node.js | 23 +++++++++-----
lib/generators/json.js | 3 +-
lib/output.js | 13 ++++++++
lib/utils/promise.js | 12 ++++++++
package.json | 2 +-
test/1-config.js | 68 ++++++++++++++++++++++++++----------------
test/4-langs.js | 31 ++++++++++++-------
test/6-parse.js | 25 ++++++++++++++++
test/mock.js | 2 ++
14 files changed, 177 insertions(+), 63 deletions(-)
diff --git a/lib/backbone/file.js b/lib/backbone/file.js
index 34cf06691..a58063cf0 100644
--- a/lib/backbone/file.js
+++ b/lib/backbone/file.js
@@ -1,3 +1,4 @@
+var _ = require('lodash');
function BackboneFile(book) {
if (!(this instanceof BackboneFile)) return new BackboneFile(book);
@@ -8,6 +9,8 @@ function BackboneFile(book) {
// Filename in the book
this.filename = '';
this.parser;
+
+ _.bindAll(this);
}
// Type of the backbone file
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index 33bd63687..a17e41379 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -1,3 +1,4 @@
+var path = require('path');
/*
A page represent a parsable file in the book (Markdown, Asciidoc, etc)
@@ -11,8 +12,12 @@ function Page(book, filename) {
}
// Return the filename of the page with another extension
+// "README.md" -> "README.html"
Page.prototype.withExtension = function(ext) {
- return
+ return path.join(
+ path.dirname(this.filename),
+ path.basename(this.filename, path.extname(this.filename)) + ext
+ );
};
diff --git a/lib/book.js b/lib/book.js
index c211adff4..477283f52 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -50,7 +50,6 @@ function Book(opts) {
this.ignore.addPattern([
// Skip Git stuff
'.git/',
- '.gitignore',
// Skip OS X meta data
'.DS_Store',
@@ -62,12 +61,7 @@ function Book(opts) {
'_book',
'*.pdf',
'*.epub',
- '*.mobi',
-
- // Skip config files
- '.ignore',
- '.bookignore',
- 'book.json'
+ '*.mobi'
]);
// Create a logger for the book
@@ -124,7 +118,7 @@ Book.prototype.isFileInScope = function(filename) {
Book.prototype.parseIgnoreRules = function() {
var that = this;
- return Promise.series([
+ return Promise.serie([
'.ignore',
'.gitignore',
'.bookignore'
@@ -132,6 +126,8 @@ Book.prototype.parseIgnoreRules = function() {
return that.readFile(filename)
.then(function(content) {
that.ignore.addPattern(content.toString().split(/\r?\n/));
+ }, function() {
+ return Promise();
});
});
};
@@ -158,7 +154,7 @@ Book.prototype.parse = function() {
that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
// Create a new book for each language and parse it
- return Promise.serial(that.langs.list(), function(lang) {
+ return Promise.serie(that.langs.list(), function(lang) {
that.log.debug.ln('Preparing book for language', lang.id);
var langBook = new Book({
fs: that.fs,
diff --git a/lib/config/index.js b/lib/config/index.js
index 267f65042..7734392e3 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -1,9 +1,15 @@
-var Q = require('q');
var _ = require('lodash');
var semver = require('semver');
var gitbook = require('../gitbook');
var configDefault = require('./default');
+var Promise = require('../utils/promise');
+
+// Config files to tested (sorted)
+var CONFIG_FILES = [
+ 'book.js',
+ 'book.json'
+];
/*
Config is an interface for the book's configuration stored in "book.json" (or "book.js")
@@ -13,6 +19,7 @@ function Config(book, baseConfig) {
this.book = book;
this.fs = book.fs;
this.log = book.log;
+ this.filename = null;
this.replace(baseConfig || {});
}
@@ -22,11 +29,17 @@ function Config(book, baseConfig) {
Config.prototype.load = function() {
var that = this;
- this.log.debug.ln('loading configuration');
- return this.fs.loadAsObject(this.book.resolve('book'))
- .fail(function(err) {
- if (err.code != 'MODULE_NOT_FOUND') throw(err);
- else return Q({});
+ this.log.info.ln('loading configuration');
+
+ // Try all potential configuration file
+ return Promise.some(CONFIG_FILES, function(filename) {
+ that.log.debug.ln('try loading configuration from', filename);
+
+ return that.fs.loadAsObject(that.book.resolve(filename))
+ .fail(function(err) {
+ if (err.code != 'MODULE_NOT_FOUND') throw(err);
+ else return Promise(false);
+ });
})
.then(function(_config) {
return that.replace(_config);
@@ -86,6 +99,11 @@ Config.prototype.replace = function(options) {
});
};
+// Return true if book has a configuration file
+Config.prototype.exists = function() {
+ return Boolean(this.filename);
+};
+
// Return path to a structure file
// Strip the extension by default
Config.prototype.getStructure = function(name, dontStripExt) {
diff --git a/lib/fs/index.js b/lib/fs/index.js
index 1961b07e2..7b8fc46e8 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -1,9 +1,10 @@
-var Q = require('q');
var _ = require('lodash');
var path = require('path');
var Buffer = require('buffer').Buffer;
var destroy = require('destroy');
+var Promise = require('../utils/promise');
+
/*
A filesystem is an interface to read/write files
GitBook can works with a virtual filesystem, for example in the browser.
@@ -73,7 +74,7 @@ FS.prototype.listAllFiles = function(folder) {
});
}
});
- }, Q([]));
+ }, Promise([]));
});
};
@@ -88,7 +89,7 @@ FS.prototype.readAsString = function(filename) {
// Write a stream to a file and returns a promise
FS.prototype.writeStream = function(filename, stream) {
var bufs = [];
- var d = Q.defer();
+ var d = Promise.defer();
var cleanup = function() {
destroy(stream);
diff --git a/lib/fs/node.js b/lib/fs/node.js
index 0c470d728..28383cf7a 100644
--- a/lib/fs/node.js
+++ b/lib/fs/node.js
@@ -1,9 +1,10 @@
-var Q = require('q');
var _ = require('lodash');
var util = require('util');
var path = require('path');
var fs = require('fs');
+var mkdirp = require('mkdirp');
+var Promise = require('../utils/promise');
var BaseFS = require('./');
function NodeFS() {
@@ -13,7 +14,7 @@ util.inherits(NodeFS, BaseFS);
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
NodeFS.prototype.exists = function(filename) {
- var d = Q.defer();
+ var d = Promise.defer();
fs.exists(filename, function(exists) {
d.resolve(exists);
@@ -24,17 +25,25 @@ NodeFS.prototype.exists = function(filename) {
// Read a file and returns a promise with the content as a buffer
NodeFS.prototype.read = function(filename) {
- return Q.nfcall(fs.readFile, filename);
+ return Promise.nfcall(fs.readFile, filename);
};
// Write a file and returns a promise
NodeFS.prototype.write = function(filename, buffer) {
- return Q.nfcall(fs.writeFile, filename, buffer);
+ var folder = path.dirname(filename);
+ return Promise()
+ .then(function() {
+ if (!folder) return;
+ return Promise.nfcall(mkdirp, folder);
+ })
+ .then(function() {
+ return Promise.nfcall(fs.writeFile, filename, buffer);
+ });
};
// List files in a directory
NodeFS.prototype.readdir = function(folder) {
- return Q.nfcall(fs.readdir, folder)
+ return Promise.nfcall(fs.readdir, folder)
.then(function(files) {
return _.chain(files)
.map(function(file) {
@@ -51,7 +60,7 @@ NodeFS.prototype.readdir = function(folder) {
// Load a JSON/JS file
NodeFS.prototype.loadAsObject = function(filename) {
- return Q()
+ return Promise()
.then(function() {
var jsFile;
@@ -64,7 +73,7 @@ NodeFS.prototype.loadAsObject = function(filename) {
return require(jsFile);
}
catch(err) {
- return Q.reject(err);
+ return Promise.reject(err);
}
});
};
diff --git a/lib/generators/json.js b/lib/generators/json.js
index 820574b49..19182bc8e 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -8,9 +8,10 @@ util.inherits(JSONGenerator, Generator);
// Write a page (parsable file)
Generator.prototype.writePage = function(page) {
+ var json = {};
- return this.output.writeFile(page.withExtension('.json'));
+ return this.output.writeFile(page.withExtension('.json'), JSON.stringify(json, null, 4));
};
diff --git a/lib/output.js b/lib/output.js
index d09f29de9..c22e8de1b 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1,3 +1,5 @@
+var Ignore = require('ignore');
+
var Promise = require('./utils/promise');
var generators = require('./generators');
var PluginsManager = require('./plugins');
@@ -9,6 +11,17 @@ function Output(book, type) {
this.type = type;
this.plugins = new PluginsManager(book);
this.generator = new generators[type](this, type);
+
+ // Files to ignore in output
+ this.ignore = Ignore();
+ this.ignore.addPattern([
+ '.gitignore',
+ '.ignore',
+ '.bookignore',
+
+ // The configuration file should not be copied in the output
+ this.book.config.filename
+ ]);
}
// Write a file to the output folder
diff --git a/lib/utils/promise.js b/lib/utils/promise.js
index c25b34935..82f4a6006 100644
--- a/lib/utils/promise.js
+++ b/lib/utils/promise.js
@@ -21,6 +21,18 @@ function serie(arr, iter, base) {
}, []);
}
+// Iter over an array and return first result (not null)
+function some(arr, iter) {
+ return _.reduce(arr, function(prev, elem, i) {
+ return prev.then(function(val) {
+ if (val) return val;
+
+ return iter(elem, i);
+ });
+ }, Q());
+}
+
module.exports = Q;
module.exports.reduce = reduce;
module.exports.serie = serie;
+module.exports.some = some;
diff --git a/package.json b/package.json
index 14f9f2411..7129e7392 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"lodash": "3.10.1",
"graceful-fs": "3.0.5",
"resolve": "0.6.3",
- "fs-extra": "0.16.5",
+ "mkdirp": "0.5.1",
"fstream-ignore": "1.0.2",
"gitbook-parsers": "0.8.9",
"gitbook-plugin-highlight": "1.0.3",
diff --git a/test/1-config.js b/test/1-config.js
index 474ffea01..3ec76ca07 100644
--- a/test/1-config.js
+++ b/test/1-config.js
@@ -2,38 +2,56 @@ var mock = require('./mock');
describe('Config', function() {
- describe('config.load()', function() {
- it('should not fail if no configuration file', function() {
+ describe('No configuration', function() {
+ var book;
+
+ before(function() {
return mock.setupDefaultBook()
- .then(function(book) {
+ .then(function(_book) {
+ book = _book;
return book.prepareConfig();
});
});
- it('should load from a JSON file', function() {
- return mock.setupDefaultBook({
- 'book.json': { title: 'Hello World' }
- })
- .then(function(book) {
- return book.prepareConfig()
- .then(function() {
- book.config.get('title', '').should.equal('Hello World');
- });
- });
- });
-
- it('should load from a JS file', function() {
- return mock.setupDefaultBook({
- 'book.js': 'module.exports = { title: "Hello World" };'
- })
- .then(function(book) {
- return book.prepareConfig()
- .then(function() {
- book.config.get('title', '').should.equal('Hello World');
- });
- });
+ it('should signal that configuration is not defined', function() {
+ book.config.exists().should.not.be.ok();
});
});
+ describe('JSON file', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'book.json': { title: 'Hello World' }
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.prepareConfig();
+ });
+ });
+
+ it('should correctly extend configuration', function() {
+ book.config.get('title', '').should.equal('Hello World');
+ });
+ });
+
+ describe('JS file', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'book.js': 'module.exports = { title: "Hello World" };'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.prepareConfig();
+ });
+ });
+
+ it('should correctly extend configuration', function() {
+ book.config.get('title', '').should.equal('Hello World');
+ });
+ });
});
diff --git a/test/4-langs.js b/test/4-langs.js
index 599cb4364..dbde992b7 100644
--- a/test/4-langs.js
+++ b/test/4-langs.js
@@ -18,18 +18,29 @@ describe('Langs', function() {
});
});
- it('should parse languages list', function() {
- return mock.setupDefaultBook({
- 'LANGS.md': '# Languages\n\n'
- + '* [en](./en)\n'
- + '* [fr](./fr)\n\n'
- })
- .then(function(book) {
- return book.langs.load()
- .then(function() {
- book.langs.count().should.equal(2);
+ describe('Non-empty languages list', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'LANGS.md': '# Languages\n\n'
+ + '* [en](./en)\n'
+ + '* [fr](./fr)\n\n'
+ })
+ .then(function(_book) {
+ book = _book;
+
+ return book.langs.load();
});
});
+
+ it('should correctly count languages', function() {
+ book.langs.count().should.equal(2);
+ });
+
+ it('should correctly define book as multilingual', function() {
+ book.isMultilingual().should.equal(true);
+ });
});
});
diff --git a/test/6-parse.js b/test/6-parse.js
index 422744d3f..a57572036 100644
--- a/test/6-parse.js
+++ b/test/6-parse.js
@@ -18,5 +18,30 @@ describe('Parsing', function() {
return book.parse().should.be.rejected;
});
});
+
+
+ describe('Multilingual book', function() {
+ var book;
+
+ before(function() {
+ return mock.setupBook({
+ 'LANGS.md': '# Languages\n\n'
+ + '* [English](./en)\n'
+ + '* [French](./fr)\n\n',
+ 'en/README.md': '# English',
+ 'en/SUMMARY.md': '# Summary',
+ 'fr/README.md': '# French',
+ 'fr/SUMMARY.md': '# Summary'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.parse();
+ });
+ });
+
+ it('should list language books', function() {
+
+ });
+ });
});
diff --git a/test/mock.js b/test/mock.js
index f562fa9b1..919a99253 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -35,11 +35,13 @@ function setupFS(fs, rootFolder, files) {
// Setup a mock book for testing using a map of files
function setupBook(files, opts) {
opts = opts || {};
+ opts.log = function() { };
return Q.nfcall(tmp.dir.bind(tmp)).get(0)
.then(function(folder) {
opts.fs = fs;
opts.root = folder;
+
return setupFS(fs, folder, files);
})
.then(function(fs) {
From 328c3a7d5141abdeab1488d6de8a47e62ebf48dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 18:42:21 +0100
Subject: [PATCH 011/217] Correctly create sub-book
---
lib/book.js | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/lib/book.js b/lib/book.js
index 477283f52..ce368a3bf 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -16,7 +16,7 @@ var Promise = require('./utils/promise');
function Book(opts) {
if (!(this instanceof Book)) return new Book(opts);
- opts = _.defaults(opts || {}, {
+ this.opts = _.defaults(opts || {}, {
fs: null,
// Root path for the book
@@ -156,11 +156,10 @@ Book.prototype.parse = function() {
// Create a new book for each language and parse it
return Promise.serie(that.langs.list(), function(lang) {
that.log.debug.ln('Preparing book for language', lang.id);
- var langBook = new Book({
- fs: that.fs,
+ var langBook = new Book(_.extend({}, that.opts, {
parent: that,
root: that.resolve(lang.id)
- });
+ }));
that.books.push(langBook);
From ea98444dc0a6e59e79408963e1d340d7765ba9f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 23:19:39 +0100
Subject: [PATCH 012/217] Add parsing of summary and tests
---
lib/backbone/article.js | 28 ------------
lib/backbone/summary.js | 99 ++++++++++++++++++++++++++++++++++++++++-
lib/utils/location.js | 21 +++++++++
test/3-glossary.js | 2 +-
test/5-summary.js | 40 +++++++++++++++++
test/6-parse.js | 3 +-
6 files changed, 161 insertions(+), 32 deletions(-)
delete mode 100644 lib/backbone/article.js
create mode 100644 lib/utils/location.js
create mode 100644 test/5-summary.js
diff --git a/lib/backbone/article.js b/lib/backbone/article.js
deleted file mode 100644
index 299df25b9..000000000
--- a/lib/backbone/article.js
+++ /dev/null
@@ -1,28 +0,0 @@
-var url = require('url');
-var _ = require('lodash');
-
-/*
-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 (optional)
-
-*/
-
-function Article(title, ref, articles) {
- var parts = url.parse(ref);
-
- this.title = title;
- this.filename = parts.pathname;
- this.anchor = parts.hash;
- this.articles = _.map(articles || [], function(article) {
- if (article instanceof Article) return article;
- return new Article(article.title, article.ref, article.articles);
- });
-}
-
-// Return true if has children
-Article.prototype.hasChildren = function() {
- return this.articles.length > 0;
-};
-
-
-module.exports = Article;
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 96815a6d3..43a373a8d 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -1,16 +1,111 @@
+var _ = require('lodash');
var util = require('util');
+var url = require('url');
+
+var location = require('../utils/location');
var BackboneFile = require('./file');
-var Article = require('./article');
+/*
+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(summary, title, ref, articles, parent) {
+ this.summary = summary;
+ this.title = title;
+
+ if (ref && location.isExternal(ref)) {
+ throw new Error('SUMMARY can only contains relative locations');
+ }
+ if (!title) {
+ throw new Error('SUMMARY entries should have an non-empty title');
+ }
+
+ var parts = url.parse(ref);
+ this.filename = parts.pathname;
+ this.anchor = parts.hash;
+
+ this.articles = _.map(articles || [], function(article) {
+ if (article instanceof TOCArticle) return article;
+ return new TOCArticle(article.title, article.ref, article.articles, this);
+ }, this);
+}
+
+// Iterate over all articles in this articles
+TOCArticle.prototype.walk = function(iter) {
+ _.each(this.articles, function(article) {
+ iter(article);
+ article.walk(iter);
+ });
+};
+
+// Return true if has children
+TOCArticle.prototype.hasChildren = function() {
+ return this.articles.length > 0;
+};
+
+/*
+A part of a ToC is a composed of a tree of articles.
+*/
+function TOCPart(summary, part) {
+ var that = this;
+
+ this.summary = summary;
+ this.articles = _.map(part.articles || part.chapters, function(article) {
+ return new TOCArticle(that.summary, article.title, article.path, article.articles);
+ });
+}
+
+// Iterate over all entries of the part
+TOCPart.prototype.walk = function(iter) {
+ _.each(this.articles, function(article) {
+ iter(article);
+ article.walk(iter);
+ });
+};
+
+/*
+A summary is composed of a list of parts, each composed wit a tree of articles.
+*/
function Summary() {
BackboneFile.apply(this, arguments);
- this.articles = [];
+ this.parts = [];
+ this._length = 0;
}
util.inherits(Summary, BackboneFile);
Summary.prototype.type = 'summary';
+// Parse the summary content
+Summary.prototype.parse = function(content) {
+ var that = this;
+
+ return this.parser.summary(content)
+
+ // TODO: update GitBook's parsers to return a list of parts
+ .then(function(part) {
+ that.parts = [new TOCPart(that, part)];
+
+ // Update count of articles
+ that._length = 0;
+ that.walk(function() {
+ that._length += 1;
+ });
+ });
+};
+
+// Iterate over all entries of the summary
+// iter is called with an TOCArticle
+Summary.prototype.walk = function(iter) {
+ _.each(this.parts, function(part) {
+ part.walk(iter);
+ });
+};
+
+// Return the count of articles in the summary
+Summary.prototype.count = function() {
+ return this._length;
+};
module.exports = Summary;
diff --git a/lib/utils/location.js b/lib/utils/location.js
new file mode 100644
index 000000000..d57e84fb4
--- /dev/null
+++ b/lib/utils/location.js
@@ -0,0 +1,21 @@
+var url = require('url');
+
+// Is the url an external url
+function isExternal(href) {
+ try {
+ return Boolean(url.parse(href).protocol);
+ } catch(err) {
+ return false;
+ }
+}
+
+
+// Inverse of isExternal
+function isRelative(href) {
+ return !isExternal(href);
+}
+
+module.exports = {
+ isExternal: isExternal,
+ isRelative: isRelative
+};
diff --git a/test/3-glossary.js b/test/3-glossary.js
index 176298ff5..0efef34c1 100644
--- a/test/3-glossary.js
+++ b/test/3-glossary.js
@@ -61,7 +61,7 @@ describe('Glossary', function() {
entry.id.should.equal('hello_world');
});
- it('should undefined return non existing entry', function() {
+ it('should return undefined for non existing entry', function() {
var entry = book.glossary.find('Hello');
should.not.exist(entry);
});
diff --git a/test/5-summary.js b/test/5-summary.js
new file mode 100644
index 000000000..2744c430a
--- /dev/null
+++ b/test/5-summary.js
@@ -0,0 +1,40 @@
+var mock = require('./mock');
+
+describe('Summary / Table of contents', function() {
+ describe('Empty summary list', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({})
+ .then(function(_book) {
+ book = _book;
+ return book.summary.load();
+ });
+ });
+
+ it('should correctly count articles', function() {
+ book.summary.count().should.equal(1);
+ });
+ });
+
+ describe('Non-empty summary list', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'SUMMARY.md': '# Summary\n\n'
+ + '* [Hello](./hello.md)\n'
+ + '* [World](./world.md)\n\n'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.summary.load();
+ });
+ });
+
+ it('should correctly count articles', function() {
+ book.summary.count().should.equal(3);
+ });
+ });
+});
+
diff --git a/test/6-parse.js b/test/6-parse.js
index a57572036..72e026054 100644
--- a/test/6-parse.js
+++ b/test/6-parse.js
@@ -40,7 +40,8 @@ describe('Parsing', function() {
});
it('should list language books', function() {
-
+ book.isMultilingual().should.equal(true);
+ book.books.should.have.lengthOf(2);
});
});
});
From ff30ba62ba694658d1575b0cf3a0fbf3d5e00d62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 23:23:56 +0100
Subject: [PATCH 013/217] Add order for tests
---
package.json | 2 +-
test/all.js | 11 +++++++++++
test/{1-config.js => config.js} | 0
test/{3-glossary.js => glossary.js} | 0
test/{4-langs.js => langs.js} | 0
test/output-json.js | 6 ++++++
test/{6-parse.js => parse.js} | 0
test/{2-readme.js => readme.js} | 0
test/{5-summary.js => summary.js} | 0
9 files changed, 18 insertions(+), 1 deletion(-)
create mode 100644 test/all.js
rename test/{1-config.js => config.js} (100%)
rename test/{3-glossary.js => glossary.js} (100%)
rename test/{4-langs.js => langs.js} (100%)
create mode 100644 test/output-json.js
rename test/{6-parse.js => parse.js} (100%)
rename test/{2-readme.js => readme.js} (100%)
rename test/{5-summary.js => summary.js} (100%)
diff --git a/package.json b/package.json
index 7129e7392..f5377d131 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
"gitbook-markdown-css": "1.0.1"
},
"scripts": {
- "test": "node_modules/.bin/mocha --reporter spec --timeout 15000"
+ "test": "node_modules/.bin/mocha --reporter spec --timeout 15000 ./test/all.js"
},
"repository": {
"type": "git",
diff --git a/test/all.js b/test/all.js
new file mode 100644
index 000000000..0c1debacb
--- /dev/null
+++ b/test/all.js
@@ -0,0 +1,11 @@
+
+// Parsing
+require('./config');
+require('./readme');
+require('./summary');
+require('./glossary');
+require('./langs');
+require('./parse');
+
+// Output
+require('./output-json');
diff --git a/test/1-config.js b/test/config.js
similarity index 100%
rename from test/1-config.js
rename to test/config.js
diff --git a/test/3-glossary.js b/test/glossary.js
similarity index 100%
rename from test/3-glossary.js
rename to test/glossary.js
diff --git a/test/4-langs.js b/test/langs.js
similarity index 100%
rename from test/4-langs.js
rename to test/langs.js
diff --git a/test/output-json.js b/test/output-json.js
new file mode 100644
index 000000000..8d276c627
--- /dev/null
+++ b/test/output-json.js
@@ -0,0 +1,6 @@
+var mock = require('./mock');
+
+describe('JSON Output', function() {
+
+});
+
diff --git a/test/6-parse.js b/test/parse.js
similarity index 100%
rename from test/6-parse.js
rename to test/parse.js
diff --git a/test/2-readme.js b/test/readme.js
similarity index 100%
rename from test/2-readme.js
rename to test/readme.js
diff --git a/test/5-summary.js b/test/summary.js
similarity index 100%
rename from test/5-summary.js
rename to test/summary.js
From e83d63c2aa5e30c26ada888990b263e6b786d3f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 28 Jan 2016 23:42:00 +0100
Subject: [PATCH 014/217] Index page of summary when parsing
---
lib/backbone/page.js | 7 ++++++-
lib/backbone/summary.js | 5 +++++
lib/book.js | 14 ++++++++++++--
lib/generators/json.js | 2 +-
lib/generators/website.js | 27 +++++++++++++++++++++++++++
lib/output.js | 14 ++++++++++++--
6 files changed, 63 insertions(+), 6 deletions(-)
create mode 100644 lib/generators/website.js
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index a17e41379..bf3494a7b 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -5,7 +5,7 @@ A page represent a parsable file in the book (Markdown, Asciidoc, etc)
*/
function Page(book, filename) {
- if (!(this instanceof Page)) return new Page();
+ if (!(this instanceof Page)) return new Page(book, filename);
this.book = book;
this.filename = filename;
@@ -20,5 +20,10 @@ Page.prototype.withExtension = function(ext) {
);
};
+// Read the page as a string
+Page.prototype.read = function() {
+ return this.book.readFile(this.filename);
+};
+
module.exports = Page;
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 43a373a8d..009fb76de 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -39,6 +39,11 @@ TOCArticle.prototype.walk = function(iter) {
});
};
+// Return true if is pointing to a file
+TOCArticle.prototype.hasLocation = function() {
+ return Boolean(this.filename);
+};
+
// Return true if has children
TOCArticle.prototype.hasChildren = function() {
return this.articles.length > 0;
diff --git a/lib/book.js b/lib/book.js
index ce368a3bf..913df2f6d 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -168,6 +168,8 @@ Book.prototype.parse = function() {
}
return Promise()
+
+ // Parse the readme
.then(that.readme.load)
.then(function() {
if (that.readme.exists()) return;
@@ -175,13 +177,21 @@ Book.prototype.parse = function() {
throw new Error('No README file (or is ignored)');
})
+ // Parse the summary
.then(that.summary.load)
.then(function() {
- if (that.summary.exists()) return;
+ if (!that.summary.exists()) {
+ throw new Error('No SUMMARY file (or is ignored)');
+ }
- throw new Error('No SUMMARY file (or is ignored)');
+ // Index summary's articles
+ that.summary.walk(function(article) {
+ if (!article.hasLocation()) return;
+ that.addPage(article.filename);
+ });
})
+ // Parse the glossary
.then(that.glossary.load);
});
};
diff --git a/lib/generators/json.js b/lib/generators/json.js
index 19182bc8e..d2c69546f 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -7,7 +7,7 @@ function JSONGenerator() {
util.inherits(JSONGenerator, Generator);
// Write a page (parsable file)
-Generator.prototype.writePage = function(page) {
+JSONGenerator.prototype.writePage = function(page) {
var json = {};
diff --git a/lib/generators/website.js b/lib/generators/website.js
new file mode 100644
index 000000000..a2c33113d
--- /dev/null
+++ b/lib/generators/website.js
@@ -0,0 +1,27 @@
+var util = require('util');
+var Generator = require('./base');
+
+function WebsiteGenerator() {
+ Generator.apply(this, arguments);
+}
+util.inherits(WebsiteGenerator, Generator);
+
+// Copy an asset file
+WebsiteGenerator.prototype.writeAsset = function(filename) {
+ var that = this;
+
+ return that.book.readFile(filename)
+ .then(function(buf) {
+ return that.output.writeFile(filename, buf);
+ });
+};
+
+// Write a page (parsable file)
+WebsiteGenerator.prototype.writePage = function(page) {
+
+};
+
+
+
+
+module.exports = WebsiteGenerator;
diff --git a/lib/output.js b/lib/output.js
index c22e8de1b..89233da89 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1,6 +1,9 @@
+var _ = require('lodash');
+var fs = require('fs');
var Ignore = require('ignore');
var Promise = require('./utils/promise');
+var pathUtil = require('./utils/path');
var generators = require('./generators');
var PluginsManager = require('./plugins');
@@ -24,9 +27,16 @@ function Output(book, type) {
]);
}
-// Write a file to the output folder
-Output.prototype.writeFile = function(filename, buf) {
+// Resolve a file in the output directory
+Output.prototype.resolve = function(filename) {
+ return pathUtil.resolveInRoot.apply(null, [this.book.config.get('output')].concat(_.toArray(arguments)));
+};
+
+// Write a file/buffer to the output folder
+Output.prototype.writeFile = function(filename, buf) {
+ filename = this.resolve(filename);
+ return Promise.nfcall(fs.writeFileSync, filename, buf);
};
// Start the generation, for a parsed book
From f32d03eb88d66b37874c461c6327b4576327b4db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Fri, 29 Jan 2016 12:21:57 +0100
Subject: [PATCH 015/217] Add base for loading plugins
---
lib/backbone/page.js | 5 ++++
lib/generators/json.js | 14 ++++++++--
lib/plugins/manager.js | 51 +++++++++++++++++++++++++++++++++
lib/plugins/plugin.js | 62 ++++++++++++++++++++++++++++++++++++++++-
lib/templating/index.js | 1 -
5 files changed, 129 insertions(+), 4 deletions(-)
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index bf3494a7b..eb3cd6114 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -25,5 +25,10 @@ Page.prototype.read = function() {
return this.book.readFile(this.filename);
};
+// Parse the page and return its content
+Page.prototype.parse = function() {
+
+};
+
module.exports = Page;
diff --git a/lib/generators/json.js b/lib/generators/json.js
index d2c69546f..b76a62e5e 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -8,10 +8,20 @@ util.inherits(JSONGenerator, Generator);
// Write a page (parsable file)
JSONGenerator.prototype.writePage = function(page) {
- var json = {};
+ var that = this;
+ // Parse the page
+ return page.parse()
- return this.output.writeFile(page.withExtension('.json'), JSON.stringify(json, null, 4));
+ // Write as json
+ .then(function() {
+ var json = {};
+
+ return this.output.writeFile(
+ page.withExtension('.json'),
+ JSON.stringify(json, null, 4)
+ );
+ });
};
diff --git a/lib/plugins/manager.js b/lib/plugins/manager.js
index 8617044cf..b6549d663 100644
--- a/lib/plugins/manager.js
+++ b/lib/plugins/manager.js
@@ -1,10 +1,61 @@
+var _ = require('lodash');
+var Promise = require('../utils/promise');
var BookPlugin = require('./plugin');
+
+/*
+PluginsManager is an interface to work with multiple plugins at once:
+- Extract assets from plugins
+- Call hooks for all plugins, etc
+*/
+
function PluginsManager(book) {
this.book = book;
this.plugins = [];
}
+// Returns a plugin by its name
+PluginsManager.prototype.get = function(name) {
+ return _.find(this.plugins, {
+ id: name
+ });
+};
+
+// Load a plugin, or a list of plugins
+PluginsManager.prototype.load = function(name) {
+ var that = this;
+
+ if (!_.isArray(name)) {
+ return Promise.serie(name, function(_name) {
+ return that.load(_name);
+ });
+ }
+
+ return Promise()
+
+ // Initiate and load the plugin
+ .then(function() {
+ var plugin;
+
+ if (!_.isString(name)) plugin = name;
+ else plugin = new BookPlugin(that.book, name);
+
+ if (that.get(plugin.id)) {
+ throw new Error('Plugin "'+plugin.id+'" is already loaded');
+ }
+
+
+ if (plugin.isLoaded()) return plugin;
+ else return plugin.load()
+ .thenResolve(plugin);
+ })
+
+ .then(function(plugin) {
+ that.plugins.push(plugin);
+ });
+};
+
+
module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 8397205c7..e9af9d404 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -1,8 +1,68 @@
+var path = require('path');
+var resolve = require('resolve');
-function BookPlugin() {
+var Promise = require('../utils/promise');
+var PLUGIN_PREFIX = 'gitbook-plugin-';
+// Return an absolute name for the plugin (the one on NPM)
+function npmId(name) {
+ if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
+ return [PLUGIN_PREFIX, name].join('');
}
+function BookPlugin(book, pluginId) {
+ this.book = book;
+ this.id = pluginId;
+ this.npmId = npmId(pluginId);
+
+ this.packageInfos = undefined;
+ this.content = undefined;
+
+}
+
+// Return true if plugin has been loaded correctly
+BookPlugin.prototype.isLoaded = function() {
+ return Boolean(this.packageInfos && this.content);
+};
+
+// Load this plugin
+BookPlugin.prototype.load = function() {
+ var that = this;
+
+ if (this.isLoaded()) {
+ return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
+ }
+
+ // Try loading plugins from different location
+ return Promise.some([
+ this.book.resolve('node_modules'),
+ __dirname
+ ], function(baseDir) {
+ try {
+ var res = resolve.sync(name+'/package.json', { basedir: baseDir });
+
+ that.baseDir = path.dirname(res);
+ that.packageInfos = require(res);
+ that.content = require(resolve.sync(name, { basedir: baseDir }));
+
+ return true;
+ } catch (err) {
+ if (err.code != 'MODULE_NOT_FOUND') throw(err);
+
+ that.packageInfos = undefined;
+ that.content = undefined;
+
+ return false;
+ }
+ })
+
+ .then(function() {
+ if (!that.isLoaded()) {
+ throw new Error('Couldn\'t locate plugin "' + that.id + '", Run \'gitbook install\' to install plugins from registry.');
+ }
+ });
+};
+
module.exports = BookPlugin;
diff --git a/lib/templating/index.js b/lib/templating/index.js
index 1031bdbfe..3a0fc16b3 100644
--- a/lib/templating/index.js
+++ b/lib/templating/index.js
@@ -4,7 +4,6 @@ function TemplatingEngine(book) {
this.book = book;
this.log = book.log;
-
this.nunjucks = new nunjucks.Environment(
this.loader,
{
From 633de85180b1c968edc3e884821c3de668d23506 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Fri, 29 Jan 2016 12:28:51 +0100
Subject: [PATCH 016/217] Improve plugin loading to elimate false-negative
---
lib/plugins/plugin.js | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index e9af9d404..d4d2b231c 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -14,12 +14,14 @@ function npmId(name) {
function BookPlugin(book, pluginId) {
this.book = book;
+ this.log = this.book.log;
+
this.id = pluginId;
this.npmId = npmId(pluginId);
+ this.baseDir;
this.packageInfos = undefined;
this.content = undefined;
-
}
// Return true if plugin has been loaded correctly
@@ -36,26 +38,28 @@ BookPlugin.prototype.load = function() {
}
// Try loading plugins from different location
- return Promise.some([
+ var promise = Promise.some([
this.book.resolve('node_modules'),
__dirname
], function(baseDir) {
+ // Locate plugin and load pacjage.json
try {
- var res = resolve.sync(name+'/package.json', { basedir: baseDir });
+ var res = resolve.sync(name + '/package.json', { basedir: baseDir });
that.baseDir = path.dirname(res);
that.packageInfos = require(res);
- that.content = require(resolve.sync(name, { basedir: baseDir }));
-
- return true;
} catch (err) {
- if (err.code != 'MODULE_NOT_FOUND') throw(err);
+ if (err.code != 'MODULE_NOT_FOUND') throw err;
that.packageInfos = undefined;
that.content = undefined;
return false;
}
+
+ // Load plugin JS content
+ that.content = require(resolve.sync(name, { basedir: baseDir }));
+ return true;
})
.then(function() {
@@ -63,6 +67,9 @@ BookPlugin.prototype.load = function() {
throw new Error('Couldn\'t locate plugin "' + that.id + '", Run \'gitbook install\' to install plugins from registry.');
}
});
+
+ this.log.info.log('Loading plugin "' + this.id + '" ...');
+ return this.log.info.promise('', promise);
};
module.exports = BookPlugin;
From a24aa79dbf5be625ec3fdac50aca955c0b478fec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 31 Jan 2016 17:01:50 +0100
Subject: [PATCH 017/217] Add base for templating
---
lib/backbone/page.js | 2 +-
lib/config/index.js | 12 +-
lib/generators/json.js | 2 +-
lib/output.js | 19 +-
lib/plugins/index.js | 61 +++++
lib/plugins/manager.js | 61 -----
lib/{templating => template}/blocks.js | 0
lib/template/index.js | 333 +++++++++++++++++++++++++
lib/{templating => template}/loader.js | 0
lib/templating/index.js | 27 --
lib/utils/error.js | 13 +
test/all.js | 2 +
test/mock.js | 19 +-
test/output-json.js | 4 +-
test/template.js | 32 +++
15 files changed, 488 insertions(+), 99 deletions(-)
delete mode 100644 lib/plugins/manager.js
rename lib/{templating => template}/blocks.js (100%)
create mode 100644 lib/template/index.js
rename lib/{templating => template}/loader.js (100%)
delete mode 100644 lib/templating/index.js
create mode 100644 lib/utils/error.js
create mode 100644 test/template.js
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index eb3cd6114..13b9c596f 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -27,7 +27,7 @@ Page.prototype.read = function() {
// Parse the page and return its content
Page.prototype.parse = function() {
-
+ return this.read();
};
diff --git a/lib/config/index.js b/lib/config/index.js
index 7734392e3..563f03bd8 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -36,14 +36,15 @@ Config.prototype.load = function() {
that.log.debug.ln('try loading configuration from', filename);
return that.fs.loadAsObject(that.book.resolve(filename))
+ .then(function(_config) {
+ that.filename = filename;
+ return that.replace(_config);
+ })
.fail(function(err) {
if (err.code != 'MODULE_NOT_FOUND') throw(err);
else return Promise(false);
});
})
- .then(function(_config) {
- return that.replace(_config);
- })
.then(function() {
if (!that.book.isLanguageBook()) {
if (!gitbook.satisfies(that.options.gitbook)) {
@@ -124,4 +125,9 @@ Config.prototype.set = function(key, value) {
return _.set(this.options, key, value);
};
+// Return a dump of the configuration
+Config.prototype.dump = function() {
+ return _.cloneDeep(this.options);
+};
+
module.exports = Config;
diff --git a/lib/generators/json.js b/lib/generators/json.js
index b76a62e5e..0cfaeb78b 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -17,7 +17,7 @@ JSONGenerator.prototype.writePage = function(page) {
.then(function() {
var json = {};
- return this.output.writeFile(
+ return that.output.writeFile(
page.withExtension('.json'),
JSON.stringify(json, null, 4)
);
diff --git a/lib/output.js b/lib/output.js
index 89233da89..01610c6e1 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -11,20 +11,22 @@ function Output(book, type) {
if (!generators[type]) throw new Error('Generator not found"' + type + '"');
this.book = book;
+ this.log = this.book.log;
+
this.type = type;
this.plugins = new PluginsManager(book);
this.generator = new generators[type](this, type);
// Files to ignore in output
this.ignore = Ignore();
- this.ignore.addPattern([
+ this.ignore.addPattern(_.compact([
'.gitignore',
'.ignore',
'.bookignore',
// The configuration file should not be copied in the output
this.book.config.filename
- ]);
+ ]));
}
@@ -36,16 +38,25 @@ Output.prototype.resolve = function(filename) {
// Write a file/buffer to the output folder
Output.prototype.writeFile = function(filename, buf) {
filename = this.resolve(filename);
- return Promise.nfcall(fs.writeFileSync, filename, buf);
+ return Promise.nfcall(fs.writeFile, filename, buf);
};
// Start the generation, for a parsed book
Output.prototype.generate = function() {
var that = this;
- var isMultilingual = this.isMultilingual();
+ var isMultilingual = this.book.isMultilingual();
return Promise()
+ // Load all plugins
+ .then(function() {
+ that.log.info.ln('Loading and preparing plugins');
+
+ var plugins = _.pluck(that.book.config.get('plugins'), 'name');
+
+ return that.plugins.load(plugins);
+ })
+
// Initialize the generation
.then(function() {
return that.generator.prepare();
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index e69de29bb..33a2047ae 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -0,0 +1,61 @@
+var _ = require('lodash');
+
+var Promise = require('../utils/promise');
+var BookPlugin = require('./plugin');
+
+
+/*
+PluginsManager is an interface to work with multiple plugins at once:
+- Extract assets from plugins
+- Call hooks for all plugins, etc
+*/
+
+function PluginsManager(book) {
+ this.book = book;
+ this.plugins = [];
+}
+
+// Returns a plugin by its name
+PluginsManager.prototype.get = function(name) {
+ return _.find(this.plugins, {
+ id: name
+ });
+};
+
+// Load a plugin, or a list of plugins
+PluginsManager.prototype.load = function(name) {
+ var that = this;
+
+ if (_.isArray(name)) {
+ return Promise.serie(name, function(_name) {
+ return that.load(_name);
+ });
+ }
+
+ return Promise()
+
+ // Initiate and load the plugin
+ .then(function() {
+ var plugin;
+
+ if (!_.isString(name)) plugin = name;
+ else plugin = new BookPlugin(that.book, name);
+
+ if (that.get(plugin.id)) {
+ throw new Error('Plugin "'+plugin.id+'" is already loaded');
+ }
+
+
+ if (plugin.isLoaded()) return plugin;
+ else return plugin.load()
+ .thenResolve(plugin);
+ })
+
+ .then(function(plugin) {
+ that.plugins.push(plugin);
+ });
+};
+
+
+
+module.exports = PluginsManager;
diff --git a/lib/plugins/manager.js b/lib/plugins/manager.js
deleted file mode 100644
index b6549d663..000000000
--- a/lib/plugins/manager.js
+++ /dev/null
@@ -1,61 +0,0 @@
-var _ = require('lodash');
-
-var Promise = require('../utils/promise');
-var BookPlugin = require('./plugin');
-
-
-/*
-PluginsManager is an interface to work with multiple plugins at once:
-- Extract assets from plugins
-- Call hooks for all plugins, etc
-*/
-
-function PluginsManager(book) {
- this.book = book;
- this.plugins = [];
-}
-
-// Returns a plugin by its name
-PluginsManager.prototype.get = function(name) {
- return _.find(this.plugins, {
- id: name
- });
-};
-
-// Load a plugin, or a list of plugins
-PluginsManager.prototype.load = function(name) {
- var that = this;
-
- if (!_.isArray(name)) {
- return Promise.serie(name, function(_name) {
- return that.load(_name);
- });
- }
-
- return Promise()
-
- // Initiate and load the plugin
- .then(function() {
- var plugin;
-
- if (!_.isString(name)) plugin = name;
- else plugin = new BookPlugin(that.book, name);
-
- if (that.get(plugin.id)) {
- throw new Error('Plugin "'+plugin.id+'" is already loaded');
- }
-
-
- if (plugin.isLoaded()) return plugin;
- else return plugin.load()
- .thenResolve(plugin);
- })
-
- .then(function(plugin) {
- that.plugins.push(plugin);
- });
-};
-
-
-
-module.exports = PluginsManager;
diff --git a/lib/templating/blocks.js b/lib/template/blocks.js
similarity index 100%
rename from lib/templating/blocks.js
rename to lib/template/blocks.js
diff --git a/lib/template/index.js b/lib/template/index.js
new file mode 100644
index 000000000..128e171b3
--- /dev/null
+++ b/lib/template/index.js
@@ -0,0 +1,333 @@
+var _ = require('lodash');
+var path = require('path');
+var nunjucks = require('nunjucks');
+var parsers = require('gitbook-parsers');
+
+var Promise = require('../utils/promise');
+var error = require('../utils/error');
+var gitbook = require('../gitbook');
+var defaultBlocks = require('./blocks');
+
+// Return extension name for a specific block
+function blockExtName(name) {
+ return 'Block'+name+'Extension';
+}
+
+// Normalize the result of block process function
+function normBlockResult(blk) {
+ if (_.isString(blk)) blk = { body: blk };
+ return blk;
+}
+
+function TemplateEngine(book) {
+ this.book = book;
+ this.log = book.log;
+
+ this.env = new nunjucks.Environment(
+ this.loader,
+ {
+ // Escaping is done after by the asciidoc/markdown parser
+ autoescape: false,
+
+ // Syntax
+ tags: {
+ blockStart: '{%',
+ blockEnd: '%}',
+ variableStart: '{{',
+ variableEnd: '}}',
+ commentStart: '{###',
+ commentEnd: '###}'
+ }
+ }
+ );
+
+ // List of tags shortcuts
+ this.shortcuts = [];
+
+ // Map of blocks bodies (that requires post-processing)
+ this.blockBodies = {};
+
+ // Map of added blocks
+ this.blocks = {};
+
+ // Bind methods
+ _.bindAll(this);
+
+ // Add default blocks
+ this.addBlocks(defaultBlocks);
+}
+
+// Add a new custom filter
+TemplateEngine.prototype.addFilter = function(filterName, func) {
+ try {
+ this.env.getFilter(filterName);
+ this.log.error.ln('conflict in filters, "'+filterName+'" is already set');
+ return false;
+ } catch(e) {
+ // Filter doesn't exist
+ }
+
+ this.log.debug.ln('add filter "'+filterName+'"');
+ this.env.addFilter(filterName, this.bindContext(function() {
+ var ctx = this;
+ var args = Array.prototype.slice.apply(arguments);
+ var callback = _.last(args);
+
+ Promise()
+ .then(function() {
+ return func.apply(ctx, args.slice(0, -1));
+ })
+ .nodeify(callback);
+ }), true);
+ return true;
+};
+
+// Add multiple filters at once
+TemplateEngine.prototype.addFilters = function(filters) {
+ _.each(filters, function(filter, name) {
+ this.addFilter(name, filter);
+ }, this);
+};
+
+// Return true if a block is defined
+TemplateEngine.prototype.hasBlock = function(name) {
+ return this.env.hasExtension(blockExtName(name));
+};
+
+// Remove/Disable a block
+TemplateEngine.prototype.removeBlock = function(name) {
+ if (!this.hasBlock(name)) return;
+
+ // Remove nunjucks extension
+ this.env.removeExtension(blockExtName(name));
+
+ // Cleanup shortcuts
+ this.shortcuts = _.reject(this.shortcuts, {
+ block: name
+ });
+};
+
+// Add a block
+// Using the extensions of nunjucks: https://mozilla.github.io/nunjucks/api.html#addextension
+TemplateEngine.prototype.addBlock = function(name, block) {
+ var that = this, Ext, extName;
+
+ // Block can be a simple function
+ if (_.isFunction(block)) block = { process: block };
+
+ block = _.defaults(block || {}, {
+ shortcuts: [],
+ end: 'end'+name,
+ process: _.identity,
+ blocks: []
+ });
+
+ extName = blockExtName(name);
+
+ if (this.hasBlock(name) && !defaultBlocks[name]) {
+ this.log.warn.ln('conflict in blocks, "'+name+'" is already defined');
+ }
+
+ // Cleanup previous block
+ this.removeBlock(name);
+
+ this.log.debug.ln('add block \''+name+'\'');
+ this.blocks[name] = block;
+
+ Ext = function () {
+ this.tags = [name];
+
+ this.parse = function(parser, nodes) {
+ var body = null;
+ var lastBlockName = null;
+ var lastBlockArgs = null;
+ var allBlocks = block.blocks.concat([block.end]);
+ var subbodies = {};
+
+ var tok = parser.nextToken();
+ var args = parser.parseSignature(null, true);
+ parser.advanceAfterBlockEnd(tok.value);
+
+ while (1) {
+ // Read body
+ var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
+
+ // Handle body with previous block name and args
+ if (lastBlockName) {
+ subbodies[lastBlockName] = subbodies[lastBlockName] || [];
+ subbodies[lastBlockName].push({
+ body: currentBody,
+ args: lastBlockArgs
+ });
+ } else {
+ body = currentBody;
+ }
+
+ // Read new block
+ lastBlockName = parser.peekToken().value;
+ if (lastBlockName == block.end) {
+ break;
+ }
+
+ // Parse signature and move to the end of the block
+ lastBlockArgs = parser.parseSignature(null, true);
+ parser.advanceAfterBlockEnd(lastBlockName);
+ }
+ parser.advanceAfterBlockEnd();
+
+ var bodies = [body];
+ _.each(block.blocks, function(blockName) {
+ subbodies[blockName] = subbodies[blockName] || [];
+ if (subbodies[blockName].length === 0) {
+ subbodies[blockName].push({
+ args: new nodes.NodeList(),
+ body: new nodes.NodeList()
+ });
+ }
+
+ bodies.push(subbodies[blockName][0].body);
+ });
+
+ return new nodes.CallExtensionAsync(this, 'run', args, bodies);
+ };
+
+ this.run = function(context) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var callback = args.pop();
+
+ // Extract blocks
+ var blocks = args
+ .concat([])
+ .slice(-block.blocks.length);
+
+ // Eliminate blocks from list
+ if (block.blocks.length > 0) args = args.slice(0, -block.blocks.length);
+
+ // Extract main body and kwargs
+ var body = args.pop();
+ var kwargs = _.isObject(_.last(args))? args.pop() : {};
+
+ // Extract blocks body
+ var _blocks = _.map(block.blocks, function(blockName, i){
+ return {
+ name: blockName,
+ body: blocks[i]()
+ };
+ });
+
+ Promise()
+ .then(function() {
+ return that.applyBlock(name, {
+ body: body(),
+ args: args,
+ kwargs: kwargs,
+ blocks: _blocks
+ }, context);
+ })
+
+ // process the block returned
+ .then(that.processBlock)
+ .nodeify(callback);
+ };
+ };
+
+ // Add the Extension
+ this.env.addExtension(extName, new Ext());
+
+ // Add shortcuts if any
+ if (!_.isArray(block.shortcuts)) {
+ block.shortcuts = [block.shortcuts];
+ }
+
+ _.each(block.shortcuts, function(shortcut) {
+ this.log.debug.ln('add template shortcut from "'+shortcut.start+'" to block "'+name+'" for parsers ', shortcut.parsers);
+ this.shortcuts.push({
+ block: name,
+ parsers: shortcut.parsers,
+ start: shortcut.start,
+ end: shortcut.end,
+ tag: {
+ start: name,
+ end: block.end
+ }
+ });
+ }, this);
+};
+
+// Add multiple blocks at once
+TemplateEngine.prototype.addBlocks = function(blocks) {
+ _.each(blocks, function(block, name) {
+ this.addBlock(name, block);
+ }, this);
+};
+
+// Apply a block to some content
+// This method result depends on the type of block (async or sync)
+TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
+ var func, block, r;
+
+ block = this.blocks[name];
+ if (!block) throw new Error('Block not found "'+name+'"');
+ if (_.isString(blk)) {
+ blk = {
+ body: blk
+ };
+ }
+
+ blk = _.defaults(blk, {
+ args: [],
+ kwargs: {},
+ blocks: []
+ });
+
+ // Bind and call block processor
+ func = this.bindContext(block.process);
+ r = func.call(ctx || {}, blk);
+
+ if (Promise.isPromise(r)) return r.then(normBlockResult);
+ else return normBlockResult(r);
+};
+
+
+// Render a string
+TemplateEngine.prototype.renderString = function(content, context, options) {
+ options = _.defaults(options || {}, {
+ path: null,
+ type: null
+ });
+
+ // Setup context for the template
+ context = _.extend({}, context, {
+ // Variables from book.json
+ book: this.book.config.get('variables'),
+
+ // Complete book.json
+ config: this.book.config.dump(),
+
+ // infos about gitbook
+ gitbook: {
+ version: gitbook.version,
+ generator: this.book.config.get('generator')
+ }
+ });
+
+ // Setup path and type
+ if (options.path) {
+ options.path = this.book.resolve(options.path);
+ }
+ if (!options.type && options.path) {
+ var parser = parsers.get(path.extname(options.path));
+ options.type = parser? parser.name : null;
+ }
+
+ // Replace shortcuts
+ //content = this.applyShortcuts(options.type, content);
+
+ return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options)
+ .fail(function(err) {
+ throw error.enforce(err);
+ });
+};
+
+
+module.exports = TemplateEngine;
diff --git a/lib/templating/loader.js b/lib/template/loader.js
similarity index 100%
rename from lib/templating/loader.js
rename to lib/template/loader.js
diff --git a/lib/templating/index.js b/lib/templating/index.js
deleted file mode 100644
index 3a0fc16b3..000000000
--- a/lib/templating/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-var nunjucks = require('nunjucks');
-
-function TemplatingEngine(book) {
- this.book = book;
- this.log = book.log;
-
- this.nunjucks = new nunjucks.Environment(
- this.loader,
- {
- // Escaping is done after by the asciidoc/markdown parser
- autoescape: false,
-
- // Syntax
- tags: {
- blockStart: '{%',
- blockEnd: '%}',
- variableStart: '{{',
- variableEnd: '}}',
- commentStart: '{###',
- commentEnd: '###}'
- }
- }
- );
-}
-
-
-module.exports = TemplatingEngine;
diff --git a/lib/utils/error.js b/lib/utils/error.js
new file mode 100644
index 000000000..66e20dbcf
--- /dev/null
+++ b/lib/utils/error.js
@@ -0,0 +1,13 @@
+var _ = require('lodash');
+
+// Enforce as an Error object, and cleanup message
+function enforce(err) {
+ if (_.isString(err)) err = new Error(err);
+ err.message = err.message.replace(/^Error: /, '');
+
+ return err;
+}
+
+module.exports = {
+ enforce: enforce
+};
diff --git a/test/all.js b/test/all.js
index 0c1debacb..4a5ba83cf 100644
--- a/test/all.js
+++ b/test/all.js
@@ -7,5 +7,7 @@ require('./glossary');
require('./langs');
require('./parse');
+require('./template');
+
// Output
require('./output-json');
diff --git a/test/mock.js b/test/mock.js
index 919a99253..973c74de9 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -7,6 +7,7 @@ require('should');
require('should-promised');
var Book = require('../').Book;
+var Output = require('../lib/output');
var NodeFS = require('../lib/fs/node');
// Create filesystem instance for testing
@@ -57,7 +58,23 @@ function setupDefaultBook(files, opts) {
}), opts);
}
+// Output a book with a specific generator
+function outputDefaultBook(generator, files, opts) {
+ return setupDefaultBook(files, opts)
+ .then(function(book) {
+ // Parse the book
+ return book.parse()
+
+ // Start generation
+ .then(function() {
+ var output = new Output(book, generator);
+ return output.generate();
+ });
+ });
+}
+
module.exports = {
setupBook: setupBook,
- setupDefaultBook: setupDefaultBook
+ setupDefaultBook: setupDefaultBook,
+ outputDefaultBook: outputDefaultBook
};
diff --git a/test/output-json.js b/test/output-json.js
index 8d276c627..9e70771b8 100644
--- a/test/output-json.js
+++ b/test/output-json.js
@@ -1,6 +1,8 @@
var mock = require('./mock');
describe('JSON Output', function() {
-
+ it('should correctly generate a default book', function() {
+ return mock.outputDefaultBook('json');
+ });
});
diff --git a/test/template.js b/test/template.js
new file mode 100644
index 000000000..b55693f62
--- /dev/null
+++ b/test/template.js
@@ -0,0 +1,32 @@
+var mock = require('./mock');
+var TemplateEngine = require('../lib/template');
+
+var pkg = require('../package.json');
+
+describe('Template', function() {
+ var book, tpl;
+
+ before(function() {
+ return mock.setupDefaultBook()
+ .then(function(_book) {
+ book = _book;
+ return book.parse();
+ })
+ .then(function() {
+ tpl = new TemplateEngine(book);
+ });
+ });
+
+ describe('.renderString', function() {
+ it('should render a simple string', function() {
+ return tpl.renderString('Hello World')
+ .should.be.fulfilledWith('Hello World');
+ });
+
+ it('should render with variable', function() {
+ return tpl.renderString('Version is {{ gitbook.version }}')
+ .should.be.fulfilledWith('Version is '+pkg.version);
+ });
+ });
+
+});
From 85a2ce9065e9914f8116f8b23b4fd4b6fad023b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 09:34:47 +0100
Subject: [PATCH 018/217] Add base for inner documentation
---
docs/README.md | 3 +++
docs/SUMMARY.md | 4 ++++
2 files changed, 7 insertions(+)
create mode 100644 docs/README.md
create mode 100644 docs/SUMMARY.md
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..3590f45cd
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,3 @@
+# GitBook v{{ book.version }}: Technical documentation
+
+
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
new file mode 100644
index 000000000..fcff3663b
--- /dev/null
+++ b/docs/SUMMARY.md
@@ -0,0 +1,4 @@
+# Summary
+
+* [About this document](README.md)
+
From 2b8e1c0b11c9245886abf9aa0a990c4f5db0c213 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 09:35:47 +0100
Subject: [PATCH 019/217] Add configuration for inner docs
---
book.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 book.js
diff --git a/book.js b/book.js
new file mode 100644
index 000000000..773256ad8
--- /dev/null
+++ b/book.js
@@ -0,0 +1,14 @@
+var pkg = require('./package.json');
+
+module.exports = {
+ root: './docs',
+ plugins: ['versions'],
+ pluginsConfig: {
+ versions: {
+ type: 'tags'
+ }
+ },
+ variables: {
+ version: pkg.version
+ }
+};
From 44ad70320a0d0b907702c57e560c6a60373c9bca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 09:43:36 +0100
Subject: [PATCH 020/217] Correctly setup folder when outputting
---
lib/book.js | 6 ++++++
lib/fs/node.js | 2 +-
lib/output.js | 25 ++++++++++++++++++++++---
3 files changed, 29 insertions(+), 4 deletions(-)
diff --git a/lib/book.js b/lib/book.js
index 913df2f6d..fd20a94b2 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -13,6 +13,12 @@ var Page = require('./backbone/page');
var pathUtil = require('./utils/path');
var Promise = require('./utils/promise');
+
+/*
+The Book class is an interface for parsing books content.
+It does not require to run on Node.js, isnce it only depends on the fs implementation
+*/
+
function Book(opts) {
if (!(this instanceof Book)) return new Book(opts);
diff --git a/lib/fs/node.js b/lib/fs/node.js
index 28383cf7a..5994c2e5a 100644
--- a/lib/fs/node.js
+++ b/lib/fs/node.js
@@ -1,7 +1,7 @@
var _ = require('lodash');
var util = require('util');
var path = require('path');
-var fs = require('fs');
+var fs = require('graceful-fs');
var mkdirp = require('mkdirp');
var Promise = require('../utils/promise');
diff --git a/lib/output.js b/lib/output.js
index 01610c6e1..2fb39383f 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1,6 +1,8 @@
var _ = require('lodash');
-var fs = require('fs');
+var fs = require('graceful-fs');
+var mkdirp = require('mkdirp');
var Ignore = require('ignore');
+var path = require('path');
var Promise = require('./utils/promise');
var pathUtil = require('./utils/path');
@@ -29,16 +31,28 @@ function Output(book, type) {
]));
}
+// Return path to the root folder
+Output.prototype.root = function(filename) {
+ return path.resolve(process.cwd(), this.book.config.get('output'));
+};
// Resolve a file in the output directory
Output.prototype.resolve = function(filename) {
- return pathUtil.resolveInRoot.apply(null, [this.book.config.get('output')].concat(_.toArray(arguments)));
+ return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
};
// Write a file/buffer to the output folder
Output.prototype.writeFile = function(filename, buf) {
filename = this.resolve(filename);
- return Promise.nfcall(fs.writeFile, filename, buf);
+ var folder = path.dirname(filename);
+
+ // Ensure fodler exists
+ return Promise.nfcall(mkdirp, folder)
+
+ // Write the file
+ .then(function() {
+ return Promise.nfcall(fs.writeFile, filename, buf);
+ });
};
// Start the generation, for a parsed book
@@ -57,6 +71,11 @@ Output.prototype.generate = function() {
return that.plugins.load(plugins);
})
+ // Create the output folder
+ .then(function() {
+ return Promise.nfcall(mkdirp, that.root());
+ })
+
// Initialize the generation
.then(function() {
return that.generator.prepare();
From 52c4dc49cf460dad4edc42457ba9e3ca4c7674d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 10:07:15 +0100
Subject: [PATCH 021/217] Normalize errors
---
lib/backbone/summary.js | 5 ++--
lib/book.js | 18 +++++++-------
lib/output.js | 3 ++-
lib/utils/error.js | 53 ++++++++++++++++++++++++++++++++++++++++-
lib/utils/path.js | 11 +++++----
package.json | 1 +
6 files changed, 75 insertions(+), 16 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 009fb76de..35f65e6f5 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -3,6 +3,7 @@ var util = require('util');
var url = require('url');
var location = require('../utils/location');
+var error = require('../utils/error');
var BackboneFile = require('./file');
@@ -15,10 +16,10 @@ function TOCArticle(summary, title, ref, articles, parent) {
this.title = title;
if (ref && location.isExternal(ref)) {
- throw new Error('SUMMARY can only contains relative locations');
+ throw error.ParsingError(new Error('SUMMARY can only contains relative locations'));
}
if (!title) {
- throw new Error('SUMMARY entries should have an non-empty title');
+ throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
}
var parts = url.parse(ref);
diff --git a/lib/book.js b/lib/book.js
index fd20a94b2..def724be5 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -11,6 +11,7 @@ var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
var Page = require('./backbone/page');
var pathUtil = require('./utils/path');
+var error = require('./utils/error');
var Promise = require('./utils/promise');
@@ -40,7 +41,7 @@ function Book(opts) {
logLevel: 'info'
});
- if (!opts.fs) throw new Error('Book requires a fs instance');
+ if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance'));
// Root path for the book
this.root = opts.root;
@@ -101,9 +102,10 @@ Book.prototype.prepareConfig = function() {
Book.prototype.resolve = function() {
var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
if (!this.isFileInScope(filename)) {
- var err = new Error('EACCESS: "' + filename + '" not in "' + this.root + '"');
- err.code = 'EACCESS';
- throw err;
+ throw error.FileOutOfScopeError({
+ filename: filename,
+ root: this.root
+ });
}
return filename;
@@ -154,7 +156,7 @@ Book.prototype.parse = function() {
.then(function() {
if (that.isMultilingual()) {
if (that.isLanguageBook()) {
- throw new Error('A multilingual book as a language book is forbidden');
+ throw error.ParsingError(new Error('A multilingual book as a language book is forbidden'));
}
that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
@@ -180,14 +182,14 @@ Book.prototype.parse = function() {
.then(function() {
if (that.readme.exists()) return;
- throw new Error('No README file (or is ignored)');
+ throw new error.FileNotFoundError({ filename: 'README' });
})
// Parse the summary
.then(that.summary.load)
.then(function() {
if (!that.summary.exists()) {
- throw new Error('No SUMMARY file (or is ignored)');
+ throw new error.FileNotFoundError({ filename: 'SUMMARY' });
}
// Index summary's articles
@@ -229,7 +231,7 @@ Book.prototype.isFileIgnored = function(filename) {
// Read a file in the book, throw error if ignored
Book.prototype.readFile = function(filename) {
- if (this.isFileIgnored(filename)) return Promise.reject(new Error('File "'+filename+'" is ignored'));
+ if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
return this.fs.readAsString(this.resolve(filename));
};
diff --git a/lib/output.js b/lib/output.js
index 2fb39383f..5c28ca198 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -6,11 +6,12 @@ var path = require('path');
var Promise = require('./utils/promise');
var pathUtil = require('./utils/path');
+var error = require('./utils/error');
var generators = require('./generators');
var PluginsManager = require('./plugins');
function Output(book, type) {
- if (!generators[type]) throw new Error('Generator not found"' + type + '"');
+ if (!generators[type]) throw error.GeneratorNotFoundError({ generator: type });
this.book = book;
this.log = this.book.log;
diff --git a/lib/utils/error.js b/lib/utils/error.js
index 66e20dbcf..4ccf83d0b 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -1,4 +1,6 @@
var _ = require('lodash');
+var TypedError = require('error/typed');
+var WrappedError = require('error/wrapped');
// Enforce as an Error object, and cleanup message
function enforce(err) {
@@ -8,6 +10,55 @@ function enforce(err) {
return err;
}
+// Random error wrappers during parsing/generation
+var ParsingError = WrappedError({
+ message: 'Parsing Error: {origMessage}',
+ type: 'server.parsing-failed'
+});
+var GenerationError = WrappedError({
+ message: 'Generation Error: {origMessage}',
+ type: 'server.parsing-failed'
+});
+
+// Error when output generator does not exists
+var GeneratorNotFoundError = TypedError({
+ type: 'server.404',
+ message: 'Generator "{generator}" does not exists',
+ generator: null
+});
+
+// A file does not exists
+var FileNotFoundError = TypedError({
+ type: 'server.404',
+ message: 'No "{filename}" file (or is ignored)',
+ filename: null
+});
+
+// A file is outside the scope
+var FileOutOfScopeError = TypedError({
+ type: 'server.404',
+ message: '"{filename}" not in "{root}"',
+ filename: null,
+ root: null,
+ code: 'EACCESS'
+});
+
+// Error for nunjucks templates
+var TemplateError = WrappedError({
+ message: 'Error compiling template "{filename}": {origMessage}',
+ type: 'client.template-failed',
+ filename: null
+});
+
module.exports = {
- enforce: enforce
+ enforce: enforce,
+
+ ParsingError: ParsingError,
+ GenerationError: GenerationError,
+
+ FileNotFoundError: FileNotFoundError,
+ FileOutOfScopeError: FileOutOfScopeError,
+
+ GeneratorNotFoundError: GeneratorNotFoundError,
+ TemplateError: TemplateError
};
diff --git a/lib/utils/path.js b/lib/utils/path.js
index 2d722a542..f195d6c29 100644
--- a/lib/utils/path.js
+++ b/lib/utils/path.js
@@ -1,6 +1,8 @@
var _ = require('lodash');
var path = require('path');
+var error = require('./error');
+
// Normalize a filename
function normalizePath(filename) {
return path.normalize(filename);
@@ -15,7 +17,7 @@ function isInRoot(root, filename) {
// Resolve paths in a specific folder
// Throw error if file is outside this folder
function resolveInRoot(root) {
- var input, result, err;
+ var input, result;
input = _.chain(arguments)
.toArray()
@@ -31,9 +33,10 @@ function resolveInRoot(root) {
result = path.resolve(root, input);
if (!isInRoot(root, result)) {
- err = new Error('EACCESS: "' + result + '" not in "' + root + '"');
- err.code = 'EACCESS';
- throw err;
+ throw new error.FileOutOfScopeError({
+ filename: result,
+ root: root
+ });
}
return result;
diff --git a/package.json b/package.json
index f5377d131..fa333fb09 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"graceful-fs": "3.0.5",
"resolve": "0.6.3",
"mkdirp": "0.5.1",
+ "error": "7.0.2",
"fstream-ignore": "1.0.2",
"gitbook-parsers": "0.8.9",
"gitbook-plugin-highlight": "1.0.3",
From e540d25665e1933ddd1883d0e1dd9c373ffe3cd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 10:08:36 +0100
Subject: [PATCH 022/217] Normalize templating error
---
lib/template/index.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lib/template/index.js b/lib/template/index.js
index 128e171b3..cd2735f64 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -325,7 +325,9 @@ TemplateEngine.prototype.renderString = function(content, context, options) {
return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options)
.fail(function(err) {
- throw error.enforce(err);
+ throw error.TemplateError(err, {
+ filename: options.path
+ });
});
};
From a67993ac0b08665502c67d9852124d17d66f4072 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 11:14:52 +0100
Subject: [PATCH 023/217] Add assertion for should to test existence of file
---
test/mock.js | 33 ++++++++++++++++++++++++---------
test/output-json.js | 5 ++++-
2 files changed, 28 insertions(+), 10 deletions(-)
diff --git a/test/mock.js b/test/mock.js
index 973c74de9..6d53ed842 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -1,9 +1,10 @@
var Q = require('q');
var _ = require('lodash');
+var fs = require('fs');
var tmp = require('tmp');
var path = require('path');
-require('should');
+var should = require('should');
require('should-promised');
var Book = require('../').Book;
@@ -11,9 +12,9 @@ var Output = require('../lib/output');
var NodeFS = require('../lib/fs/node');
// Create filesystem instance for testing
-var fs = new NodeFS();
+var nodeFS = new NodeFS();
-function setupFS(fs, rootFolder, files) {
+function setupFS(_fs, rootFolder, files) {
return _.chain(_.pairs(files))
.sortBy(0)
.reduce(function(prev, pair) {
@@ -24,12 +25,12 @@ function setupFS(fs, rootFolder, files) {
if (_.isObject(buf)) buf = JSON.stringify(buf);
if (_.isString(buf)) buf = new Buffer(buf, 'utf-8');
- return fs.write(filename, buf);
+ return _fs.write(filename, buf);
});
}, Q())
.value()
.then(function() {
- return fs;
+ return _fs;
});
}
@@ -40,12 +41,12 @@ function setupBook(files, opts) {
return Q.nfcall(tmp.dir.bind(tmp)).get(0)
.then(function(folder) {
- opts.fs = fs;
+ opts.fs = nodeFS;
opts.root = folder;
- return setupFS(fs, folder, files);
+ return setupFS(nodeFS, folder, files);
})
- .then(function(fs) {
+ .then(function() {
return new Book(opts);
});
}
@@ -68,11 +69,25 @@ function outputDefaultBook(generator, files, opts) {
// Start generation
.then(function() {
var output = new Output(book, generator);
- return output.generate();
+ return output.generate()
+ .thenResolve(output);
});
});
}
+// Assertions to test if an Output has generated a file
+should.Assertion.add('file', function(file, description) {
+ this.params = {
+ actual: this.obj.toString(),
+ operator: 'have file ' + file,
+ message: description
+ };
+
+ this.obj.should.have.property('resolve').which.is.a.Function;
+ this.assert(fs.existsSync(this.obj.resolve(file)));
+});
+
+
module.exports = {
setupBook: setupBook,
setupDefaultBook: setupDefaultBook,
diff --git a/test/output-json.js b/test/output-json.js
index 9e70771b8..ee345e27a 100644
--- a/test/output-json.js
+++ b/test/output-json.js
@@ -2,7 +2,10 @@ var mock = require('./mock');
describe('JSON Output', function() {
it('should correctly generate a default book', function() {
- return mock.outputDefaultBook('json');
+ return mock.outputDefaultBook('json')
+ .then(function(output) {
+ output.should.have.file('README.json');
+ });
});
});
From 02cff8c72ca742cd06a65d171657eb230eedbc5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 11:53:32 +0100
Subject: [PATCH 024/217] Add base for page parsing
---
lib/backbone/page.js | 61 +++++++++++++++++++++++++++++++++++++++----
lib/book.js | 4 +++
lib/template/index.js | 7 +++--
test/output-json.js | 16 +++++++++---
4 files changed, 76 insertions(+), 12 deletions(-)
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index 13b9c596f..787218177 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -1,4 +1,8 @@
+var _ = require('lodash');
var path = require('path');
+var parsers = require('gitbook-parsers');
+
+var error = require('../utils/error');
/*
A page represent a parsable file in the book (Markdown, Asciidoc, etc)
@@ -6,28 +10,75 @@ A page represent a parsable file in the book (Markdown, Asciidoc, etc)
function Page(book, filename) {
if (!(this instanceof Page)) return new Page(book, filename);
+ var extension;
+ _.bindAll(this);
this.book = book;
- this.filename = filename;
+ this.log = this.book.log;
+
+ this.content = '';
+ this.path = filename;
+ this.rawPath = this.book.resolve(filename);
+
+ // Can we parse it?
+ extension = path.extname(this.path);
+ this.parser = parsers.get(extension);
+ if (!this.parser) throw error.ParsingError(new Error('Can\'t parse file "'+this.path+'"'));
+
+ this.type = this.parser.name;
}
// Return the filename of the page with another extension
// "README.md" -> "README.html"
Page.prototype.withExtension = function(ext) {
return path.join(
- path.dirname(this.filename),
- path.basename(this.filename, path.extname(this.filename)) + ext
+ path.dirname(this.path),
+ path.basename(this.path, path.extname(this.path)) + ext
);
};
+// Update content of the page
+Page.prototype.update = function(content) {
+ this.content = content;
+};
+
// Read the page as a string
Page.prototype.read = function() {
- return this.book.readFile(this.filename);
+ return this.book.readFile(this.path)
+ .then(this.update);
};
// Parse the page and return its content
Page.prototype.parse = function() {
- return this.read();
+ var that = this;
+
+ this.log.debug.ln('start parsing file', this.path);
+
+ return this.read()
+
+ // Pre-process page with parser
+ .then(function() {
+ return that.parser.page.prepare(that.content)
+ .then(that.update);
+ })
+
+ // Render template
+ .then(function() {
+ return that.book.template.renderString(that.content, {
+ file: {
+ path: that.path,
+ mtime: 0 //todo: stat.mtime
+ }
+ }, {
+ file: that.path
+ })
+ .then(that.update);
+ })
+
+ // Render markup
+ .then(function() {
+ return that.parser.page(that.content);
+ });
};
diff --git a/lib/book.js b/lib/book.js
index def724be5..e762ad91a 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -10,6 +10,7 @@ var Glossary = require('./backbone/glossary');
var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
var Page = require('./backbone/page');
+var TemplateEngine = require('./template');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
var Promise = require('./utils/promise');
@@ -89,6 +90,9 @@ function Book(opts) {
// List of page in the book
this.pages = {};
+ // Templating engine
+ this.template = new TemplateEngine(this);
+
_.bindAll(this);
}
diff --git a/lib/template/index.js b/lib/template/index.js
index cd2735f64..833ff76d7 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -292,8 +292,7 @@ TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
// Render a string
TemplateEngine.prototype.renderString = function(content, context, options) {
options = _.defaults(options || {}, {
- path: null,
- type: null
+ path: null
});
// Setup context for the template
@@ -315,9 +314,9 @@ TemplateEngine.prototype.renderString = function(content, context, options) {
if (options.path) {
options.path = this.book.resolve(options.path);
}
- if (!options.type && options.path) {
+ if (!context.type && options.path) {
var parser = parsers.get(path.extname(options.path));
- options.type = parser? parser.name : null;
+ context.type = parser? parser.name : null;
}
// Replace shortcuts
diff --git a/test/output-json.js b/test/output-json.js
index ee345e27a..b2387a7a6 100644
--- a/test/output-json.js
+++ b/test/output-json.js
@@ -1,11 +1,21 @@
var mock = require('./mock');
describe('JSON Output', function() {
- it('should correctly generate a default book', function() {
- return mock.outputDefaultBook('json')
- .then(function(output) {
+
+ describe('Sample book', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook('json')
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ it('should correctly generate a README.json', function() {
output.should.have.file('README.json');
});
+
});
});
From 87d90a65ac6d1ceb5c1ce255d71713cff7dc9d17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 13:31:19 +0100
Subject: [PATCH 025/217] Stat file when parsing page
---
lib/backbone/page.js | 20 +++++++++++++++++---
lib/book.js | 6 ++++++
lib/fs/index.js | 5 +++++
lib/fs/node.js | 5 +++++
4 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index 787218177..c75c4346c 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -16,10 +16,18 @@ function Page(book, filename) {
this.book = book;
this.log = this.book.log;
+ // Current content
this.content = '';
+
+ // Relative path to the page
this.path = filename;
+
+ // Absolute path to the page
this.rawPath = this.book.resolve(filename);
+ // Last modification date
+ this.mtime = 0;
+
// Can we parse it?
extension = path.extname(this.path);
this.parser = parsers.get(extension);
@@ -44,8 +52,14 @@ Page.prototype.update = function(content) {
// Read the page as a string
Page.prototype.read = function() {
- return this.book.readFile(this.path)
- .then(this.update);
+ var that = this;
+
+ return this.book.statFile(this.path)
+ .then(function(stat) {
+ that.mtime = stat.mtime;
+ return that.book.readFile(that.path);
+ })
+ .then(this.update);
};
// Parse the page and return its content
@@ -67,7 +81,7 @@ Page.prototype.parse = function() {
return that.book.template.renderString(that.content, {
file: {
path: that.path,
- mtime: 0 //todo: stat.mtime
+ mtime: that.mtime
}
}, {
file: that.path
diff --git a/lib/book.js b/lib/book.js
index e762ad91a..f6158338e 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -239,6 +239,12 @@ Book.prototype.readFile = function(filename) {
return this.fs.readAsString(this.resolve(filename));
};
+// Get stat infos about a file
+Book.prototype.statFile = function(filename) {
+ if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
+ return this.fs.stat(this.resolve(filename));
+};
+
// Find a parsable file using a filename
Book.prototype.findParsableFile = function(filename) {
var that = this;
diff --git a/lib/fs/index.js b/lib/fs/index.js
index 7b8fc46e8..bcd9345b4 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -30,6 +30,11 @@ FS.prototype.read = function(filename) {
// To implement for each fs
};
+// Read stat infos about a file
+FS.prototype.stat = function(filename) {
+ // To implement for each fs
+};
+
// Write a file and returns a promise
FS.prototype.write = function(filename, buffer) {
// To implement for each fs
diff --git a/lib/fs/node.js b/lib/fs/node.js
index 5994c2e5a..f9a885d79 100644
--- a/lib/fs/node.js
+++ b/lib/fs/node.js
@@ -28,6 +28,11 @@ NodeFS.prototype.read = function(filename) {
return Promise.nfcall(fs.readFile, filename);
};
+// Read stat infos about a file
+NodeFS.prototype.stat = function(filename) {
+ return Promise.nfcall(fs.stat, filename);
+};
+
// Write a file and returns a promise
NodeFS.prototype.write = function(filename, buffer) {
var folder = path.dirname(filename);
From f47208528b28dad69391aa2e4eaccb32d11ba69d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 13:58:26 +0100
Subject: [PATCH 026/217] Normalize fs access
---
lib/backbone/page.js | 5 ++--
lib/fs/node.js | 21 +++++---------
lib/output.js | 32 +++++++++++++++------
lib/utils/fs.js | 66 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 100 insertions(+), 24 deletions(-)
create mode 100644 lib/utils/fs.js
diff --git a/lib/backbone/page.js b/lib/backbone/page.js
index c75c4346c..94812d991 100644
--- a/lib/backbone/page.js
+++ b/lib/backbone/page.js
@@ -89,9 +89,10 @@ Page.prototype.parse = function() {
.then(that.update);
})
- // Render markup
+ // Render markup using the parser
.then(function() {
- return that.parser.page(that.content);
+ return that.parser.page(that.content)
+ .then(that.update);
});
};
diff --git a/lib/fs/node.js b/lib/fs/node.js
index f9a885d79..330391a0a 100644
--- a/lib/fs/node.js
+++ b/lib/fs/node.js
@@ -1,9 +1,8 @@
var _ = require('lodash');
var util = require('util');
var path = require('path');
-var fs = require('graceful-fs');
-var mkdirp = require('mkdirp');
+var fs = require('../utils/fs');
var Promise = require('../utils/promise');
var BaseFS = require('./');
@@ -14,23 +13,17 @@ util.inherits(NodeFS, BaseFS);
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
NodeFS.prototype.exists = function(filename) {
- var d = Promise.defer();
-
- fs.exists(filename, function(exists) {
- d.resolve(exists);
- });
-
- return d.promise;
+ return fs.exists(filename);
};
// Read a file and returns a promise with the content as a buffer
NodeFS.prototype.read = function(filename) {
- return Promise.nfcall(fs.readFile, filename);
+ return fs.readFile(filename);
};
// Read stat infos about a file
NodeFS.prototype.stat = function(filename) {
- return Promise.nfcall(fs.stat, filename);
+ return fs.stat(filename);
};
// Write a file and returns a promise
@@ -39,16 +32,16 @@ NodeFS.prototype.write = function(filename, buffer) {
return Promise()
.then(function() {
if (!folder) return;
- return Promise.nfcall(mkdirp, folder);
+ return fs.mkdirp(folder);
})
.then(function() {
- return Promise.nfcall(fs.writeFile, filename, buffer);
+ return fs.writeFile(filename, buffer);
});
};
// List files in a directory
NodeFS.prototype.readdir = function(folder) {
- return Promise.nfcall(fs.readdir, folder)
+ return fs.readdir(folder)
.then(function(files) {
return _.chain(files)
.map(function(file) {
diff --git a/lib/output.js b/lib/output.js
index 5c28ca198..b787edc78 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1,12 +1,11 @@
var _ = require('lodash');
-var fs = require('graceful-fs');
-var mkdirp = require('mkdirp');
var Ignore = require('ignore');
var path = require('path');
var Promise = require('./utils/promise');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
+var fs = require('./utils/fs');
var generators = require('./generators');
var PluginsManager = require('./plugins');
@@ -44,15 +43,32 @@ Output.prototype.resolve = function(filename) {
// Write a file/buffer to the output folder
Output.prototype.writeFile = function(filename, buf) {
- filename = this.resolve(filename);
- var folder = path.dirname(filename);
+ var that = this;
- // Ensure fodler exists
- return Promise.nfcall(mkdirp, folder)
+ return Promise()
+ .then(function() {
+ filename = that.resolve(filename);
+ var folder = path.dirname(filename);
+
+ // Ensure fodler exists
+ return fs.mkdirp(folder);
+ })
// Write the file
.then(function() {
- return Promise.nfcall(fs.writeFile, filename, buf);
+ return fs.writeFile(filename, buf);
+ });
+};
+
+// Copy a file to the output
+Output.prototype.copyFile = function(from, to) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ to = that.resolve(to);
+
+ return fs.copy(from, to);
});
};
@@ -74,7 +90,7 @@ Output.prototype.generate = function() {
// Create the output folder
.then(function() {
- return Promise.nfcall(mkdirp, that.root());
+ return fs.mkdirp(that.root());
})
// Initialize the generation
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
new file mode 100644
index 000000000..2fa6ff980
--- /dev/null
+++ b/lib/utils/fs.js
@@ -0,0 +1,66 @@
+var fs = require('graceful-fs');
+var mkdirp = require('mkdirp');
+var destroy = require('destroy');
+
+var Promise = require('./promise');
+
+// Write a stream to a file
+function writeStream(filename, st) {
+ var d = Promise.defer();
+
+ var wstream = fs.createWriteStream(filename);
+ var cleanup = function() {
+ destroy(wstream);
+ wstream.removeAllListeners();
+ };
+
+ wstream.on('finish', function () {
+ cleanup();
+ d.resolve();
+ });
+ wstream.on('error', function (err) {
+ cleanup();
+ d.reject(err);
+ });
+
+ st.on('error', function(err) {
+ cleanup();
+ d.reject(err);
+ });
+
+ st.pipe(wstream);
+
+ return d.promise;
+}
+
+// Copy a file using stream
+function copyFile(from, to) {
+ return Promise()
+ .then(function() {
+ return writeStream(to, fs.createReadStream(from));
+ });
+}
+
+// Return a promise resolved with a boolean
+function fileExists(filename) {
+ var d = Promise.defer();
+
+ fs.exists(filename, function(exists) {
+ d.resolve(exists);
+ });
+
+ return d.promise;
+}
+
+
+module.exports = {
+ exists: fileExists,
+ mkdirp: Promise.nfbind(mkdirp),
+ readFile: Promise.nfbind(fs.readFile),
+ writeFile: Promise.nfbind(fs.writeFile),
+ stat: Promise.nfbind(fs.stat),
+ statSync: fs.statSync,
+ readdir: Promise.nfbind(fs.readdir),
+ writeStream: writeStream,
+ copy: copyFile
+};
From 09c6f025a1ddd1feeb1b2e524da7de51f2f13e17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 11 Feb 2016 14:01:52 +0100
Subject: [PATCH 027/217] For multilingual book, copy readme from main book
---
lib/generators/json.js | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/lib/generators/json.js b/lib/generators/json.js
index 0cfaeb78b..560f099dd 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -1,5 +1,6 @@
var util = require('util');
var Generator = require('./base');
+var gitbook = require('../gitbook');
function JSONGenerator() {
Generator.apply(this, arguments);
@@ -15,7 +16,13 @@ JSONGenerator.prototype.writePage = function(page) {
// Write as json
.then(function() {
- var json = {};
+ var json = {
+ gitbook: {
+ version: gitbook.version
+ },
+ path: page.path,
+ sections: page.content.sections
+ };
return that.output.writeFile(
page.withExtension('.json'),
@@ -24,7 +31,17 @@ JSONGenerator.prototype.writePage = function(page) {
});
};
+// At the end of generation, generate README.json for multilingual books
+JSONGenerator.prototype.finish = function() {
+ if (!this.book.isMultilingual()) return;
+ // Copy README.json from main book
+ var mainLanguage = this.book.langs.getDefault().id;
+ return this.output.copyFile(
+ this.output.resolve(mainLanguage, 'README.json'),
+ 'README.json'
+ );
+};
module.exports = JSONGenerator;
From 5cf0abaf2b50946420952e9ce36e357ac0d08d54 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 15:36:05 +0100
Subject: [PATCH 028/217] Add documentation for conrefs
---
docs/SUMMARY.md | 2 +-
docs/conrefs.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 56 insertions(+), 1 deletion(-)
create mode 100644 docs/conrefs.md
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index fcff3663b..d8990733e 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -1,4 +1,4 @@
# Summary
* [About this document](README.md)
-
+* [Content References](conrefs.md)
diff --git a/docs/conrefs.md b/docs/conrefs.md
new file mode 100644
index 000000000..7844e9c44
--- /dev/null
+++ b/docs/conrefs.md
@@ -0,0 +1,55 @@
+# Content References
+
+Content referencing (conref) is a convenient mechanism for reuse of content from other files or books.
+
+### Importing local files
+
+Importing an other file's content is really easy using the `include` tag:
+
+```
+{% include "./test.md" %}
+```
+
+### Importing file from another book
+
+GitBook can also resolve the include path by using git:
+
+```
+{% include "git+https://github.com/GitbookIO/documentation.git/README.md#0.0.1" %}
+```
+
+The format of git url is:
+
+```
+git+https://user@hostname/project/blah.git/file#commit-ish
+```
+
+The real git url part should finish with `.git`, the filename to import is extracted after the `.git` till the fragment of the url.
+
+The `commit-ish` can be any tag, sha, or branch which can be supplied as an argument to `git checkout`. The default is `master`.
+
+### Inheritance
+
+Template inheritance is a way to make it easy to reuse templates. When writing a template, you can define "blocks" that child templates can override. The inheritance chain can be as long as you like.
+
+`block` defines a section on the template and identifies it with a name. Base templates can specify blocks and child templates can override them with new content.
+
+```
+{% extends "./mypage.md" %}
+
+{% block pageContent %}
+# This is my page content
+{% endblock %}
+```
+
+In the file `mypage.md`, you should specify the blocks that can be extent:
+
+```
+{% block pageContent %}
+This is the default content
+{% endblock %}
+
+# License
+
+{% import "./LICENSE" %}
+```
From 347a536a4e313dece2e12d0ba1ec6654ee729c85 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 16:35:41 +0100
Subject: [PATCH 029/217] Add git urls parsing and tests
---
lib/utils/command.js | 19 +++++++
lib/utils/git.js | 119 +++++++++++++++++++++++++++++++++++++++++++
test.html | 16 ++++++
test/all.js | 1 +
test/git.js | 48 +++++++++++++++++
5 files changed, 203 insertions(+)
create mode 100644 lib/utils/command.js
create mode 100644 lib/utils/git.js
create mode 100644 test.html
create mode 100644 test/git.js
diff --git a/lib/utils/command.js b/lib/utils/command.js
new file mode 100644
index 000000000..f395fa1d1
--- /dev/null
+++ b/lib/utils/command.js
@@ -0,0 +1,19 @@
+var childProcess = require('child_process');
+var Promise = require('./promise');
+
+// On borwser, command execution is not possible
+var isAvailable = childProcess && childProcess.exec;
+
+// Execute a command
+function exec(command, options) {
+ if (!isAvailable) {
+ return Promise.reject(new Error('Command execution is not possible on this platform'));
+ }
+
+ return Promise.nfcall(childProcess.exec, command, options);
+}
+
+module.exports = {
+ isAvailable: isAvailable,
+ exec: exec
+};
diff --git a/lib/utils/git.js b/lib/utils/git.js
new file mode 100644
index 000000000..6e37b147c
--- /dev/null
+++ b/lib/utils/git.js
@@ -0,0 +1,119 @@
+var _ = require('lodash');
+var path = require('path');
+var crc = require('crc');
+var URI = require('urijs');
+var pathUtil = require('./path');
+
+var Promise = require('./promise');
+var command = require('./command');
+var fs = require('./fs');
+
+var GIT_PREFIX = 'git+';
+var GIT_TMP = null;
+
+
+// Check if an url is a git dependency url
+function isGitUrl(giturl) {
+ return (giturl.indexOf(GIT_PREFIX) === 0);
+}
+
+// Parse and extract infos
+function parseGitUrl(giturl) {
+ var ref, uri, fileParts, filepath;
+
+ if (!isGitUrl(giturl)) return null;
+ giturl = giturl.slice(GIT_PREFIX.length);
+
+ uri = new URI(giturl);
+ ref = uri.fragment() || 'master';
+ uri.fragment(null);
+
+ // Extract file inside the repo (after the .git)
+ fileParts =uri.path().split('.git');
+ filepath = fileParts.length > 1? fileParts.slice(1).join('.git') : '';
+ if (filepath[0] == '/') filepath = filepath.slice(1);
+
+ // Recreate pathname without the real filename
+ uri.path(_.first(fileParts)+'.git');
+
+ return {
+ host: uri.toString(),
+ ref: ref || 'master',
+ filepath: filepath
+ };
+}
+
+// Clone a git repo from a specific ref
+function cloneGitRepo(host, ref) {
+ ref = ref || 'master';
+
+ return Promise()
+
+ // Create temporary folder to store git repos
+ .then(function() {
+ if (GIT_TMP) return;
+ return fs.tmp.dir()
+ .then(function(_tmp) {
+ GIT_TMP = _tmp;
+ });
+ })
+
+ // Return or clone the git repo
+ .then(function() {
+ // Unique ID for repo/ref combinaison
+ var repoId = crc.crc32(host+'#'+ref).toString(16);
+
+ // Absolute path to the folder
+ var repoPath = path.resolve(GIT_TMP, repoId);
+
+ return fs.exists(repoPath)
+ .then(function(doExists) {
+ if (doExists) return;
+
+ // Clone repo
+ return command.exec('git clone '+host+' '+repoPath)
+ .then(function() {
+ return command.exec('git checkout '+ref, { cwd: repoPath });
+ });
+ })
+ .thenResolve(repoPath);
+ });
+}
+
+// Get file from a git repo
+function resolveFileFromGit(giturl) {
+ if (_.isString(giturl)) giturl = parseGitUrl(giturl);
+ if (!giturl) return Promise(null);
+
+ // Clone or get from cache
+ return cloneGitRepo(giturl.host, giturl.ref)
+ .then(function(repo) {
+
+ // Resolve relative path
+ return path.resolve(repo, giturl.filepath);
+ });
+}
+
+// Return root of git repo from a filepath
+function resolveGitRoot(filepath) {
+ var relativeToGit, repoId;
+
+ // No git repo cloned, or file is not in a git repository
+ if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null;
+
+ // Extract first directory (is the repo id)
+ relativeToGit = path.relative(GIT_TMP, filepath);
+ repoId = _.first(relativeToGit.split(path.sep));
+ if (!repoId) return;
+
+ // Return an absolute file
+ return path.resolve(GIT_TMP, repoId);
+}
+
+
+module.exports = {
+ isUrl: isGitUrl,
+ parseUrl: parseGitUrl,
+ resolveFile: resolveFileFromGit,
+ resolveRoot: resolveGitRoot
+};
diff --git a/test.html b/test.html
new file mode 100644
index 000000000..3dee7eff4
--- /dev/null
+++ b/test.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ The HTML5 Herald
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/all.js b/test/all.js
index 4a5ba83cf..0f5560300 100644
--- a/test/all.js
+++ b/test/all.js
@@ -8,6 +8,7 @@ require('./langs');
require('./parse');
require('./template');
+require('./git');
// Output
require('./output-json');
diff --git a/test/git.js b/test/git.js
new file mode 100644
index 000000000..bad031b92
--- /dev/null
+++ b/test/git.js
@@ -0,0 +1,48 @@
+var should = require('should');
+var git = require('../lib/utils/git');
+
+describe('Git', function() {
+
+ describe('URL parsing', function() {
+
+ it('should correctly validate git urls', function() {
+ // HTTPS
+ git.isUrl('git+https://github.com/Hello/world.git').should.be.ok;
+
+ // SSH
+ git.isUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1').should.be.ok;
+
+ // Non valid
+ git.isUrl('https://github.com/Hello/world.git').should.not.be.ok;
+ git.isUrl('README.md').should.not.be.ok;
+ });
+
+ it('should parse HTTPS urls', function() {
+ var parts = git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md');
+
+ should.exist(parts);
+ parts.host.should.be.equal('https://gist.github.com/69ea4542e4c8967d2fa7.git');
+ parts.ref.should.be.equal('master');
+ parts.filepath.should.be.equal('test.md');
+ });
+
+ it('should parse HTTPS urls with a reference', function() {
+ var parts = git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#1.0.0');
+
+ should.exist(parts);
+ parts.host.should.be.equal('https://gist.github.com/69ea4542e4c8967d2fa7.git');
+ parts.ref.should.be.equal('1.0.0');
+ parts.filepath.should.be.equal('test.md');
+ });
+
+ it('should parse SSH urls', function() {
+ var parts = git.parseUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1');
+
+ should.exist(parts);
+ parts.host.should.be.equal('git@github.com:GitbookIO/gitbook.git');
+ parts.ref.should.be.equal('e1594cde2c32e4ff48f6c4eff3d3d461743d74e1');
+ parts.filepath.should.be.equal('directory/README.md');
+ });
+ });
+
+});
From 7f9aa214a412f3d173ac52042bd7250aacda6143 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 17:10:59 +0100
Subject: [PATCH 030/217] Cleaner module for managing git refs
---
lib/utils/git.js | 166 ++++++++++++++++++++++-------------------------
test/git.js | 31 ++++++---
2 files changed, 101 insertions(+), 96 deletions(-)
diff --git a/lib/utils/git.js b/lib/utils/git.js
index 6e37b147c..9fca73ff7 100644
--- a/lib/utils/git.js
+++ b/lib/utils/git.js
@@ -2,34 +2,99 @@ var _ = require('lodash');
var path = require('path');
var crc = require('crc');
var URI = require('urijs');
-var pathUtil = require('./path');
+var pathUtil = require('./path');
var Promise = require('./promise');
var command = require('./command');
-var fs = require('./fs');
var GIT_PREFIX = 'git+';
-var GIT_TMP = null;
-
-// Check if an url is a git dependency url
-function isGitUrl(giturl) {
- return (giturl.indexOf(GIT_PREFIX) === 0);
+function Git(tmpDir) {
+ this.tmpDir = tmpDir;
+ this.cloned = {};
}
+// Return an unique ID for a combinaison host/ref
+Git.prototype.repoID = function(host, ref) {
+ return crc.crc32(host+'#'+(ref || '')).toString(16);
+};
+
+// Clone a git repository if non existant
+Git.prototype.clone = function(host, ref) {
+ var that = this;
+
+ return Promise()
+
+ // Return or clone the git repo
+ .then(function() {
+ // Unique ID for repo/ref combinaison
+ var repoId = that.repoID(host, ref);
+
+ // Absolute path to the folder
+ var repoPath = path.join(that.tmpDir, repoId);
+
+ if (that.cloned[repoId]) return repoPath;
+
+ // Clone repo
+ return command.exec('git clone '+host+' '+repoPath)
+
+ // Checkout reference if specified
+ .then(function() {
+ that.cloned[repoId] = true;
+
+ if (!ref) return;
+ return command.exec('git checkout '+ref, { cwd: repoPath });
+ })
+ .thenResolve(repoPath);
+ });
+};
+
+// Get file from a git repo
+Git.prototype.resolve = function(giturl) {
+ if (_.isString(giturl)) giturl = Git.parseUrl(giturl);
+ if (!giturl) return Promise(null);
+
+ // Clone or get from cache
+ return this.clone(giturl.host, giturl.ref)
+ .then(function(repo) {
+ return path.resolve(repo, giturl.filepath);
+ });
+};
+
+// Return root of git repo from a filepath
+Git.prototype.resolveRoot = function(filepath) {
+ var relativeToGit, repoId;
+
+ // No git repo cloned, or file is not in a git repository
+ if (!pathUtil.isInRoot(this.tmpDir, filepath)) return null;
+
+ // Extract first directory (is the repo id)
+ relativeToGit = path.relative(this.tmpDir, filepath);
+ repoId = _.first(relativeToGit.split(path.sep));
+ if (!repoId) return;
+
+ // Return an absolute file
+ return path.resolve(this.tmpDir, repoId);
+};
+
+// Check if an url is a git dependency url
+Git.isUrl = function(giturl) {
+ return (giturl.indexOf(GIT_PREFIX) === 0);
+};
+
// Parse and extract infos
-function parseGitUrl(giturl) {
+Git.parseUrl = function(giturl) {
var ref, uri, fileParts, filepath;
- if (!isGitUrl(giturl)) return null;
+ if (!Git.isUrl(giturl)) return null;
giturl = giturl.slice(GIT_PREFIX.length);
uri = new URI(giturl);
- ref = uri.fragment() || 'master';
+ ref = uri.fragment() || null;
uri.fragment(null);
// Extract file inside the repo (after the .git)
- fileParts =uri.path().split('.git');
+ fileParts = uri.path().split('.git');
filepath = fileParts.length > 1? fileParts.slice(1).join('.git') : '';
if (filepath[0] == '/') filepath = filepath.slice(1);
@@ -38,82 +103,9 @@ function parseGitUrl(giturl) {
return {
host: uri.toString(),
- ref: ref || 'master',
+ ref: ref,
filepath: filepath
};
-}
-
-// Clone a git repo from a specific ref
-function cloneGitRepo(host, ref) {
- ref = ref || 'master';
-
- return Promise()
-
- // Create temporary folder to store git repos
- .then(function() {
- if (GIT_TMP) return;
- return fs.tmp.dir()
- .then(function(_tmp) {
- GIT_TMP = _tmp;
- });
- })
-
- // Return or clone the git repo
- .then(function() {
- // Unique ID for repo/ref combinaison
- var repoId = crc.crc32(host+'#'+ref).toString(16);
-
- // Absolute path to the folder
- var repoPath = path.resolve(GIT_TMP, repoId);
-
- return fs.exists(repoPath)
- .then(function(doExists) {
- if (doExists) return;
-
- // Clone repo
- return command.exec('git clone '+host+' '+repoPath)
- .then(function() {
- return command.exec('git checkout '+ref, { cwd: repoPath });
- });
- })
- .thenResolve(repoPath);
- });
-}
-
-// Get file from a git repo
-function resolveFileFromGit(giturl) {
- if (_.isString(giturl)) giturl = parseGitUrl(giturl);
- if (!giturl) return Promise(null);
-
- // Clone or get from cache
- return cloneGitRepo(giturl.host, giturl.ref)
- .then(function(repo) {
-
- // Resolve relative path
- return path.resolve(repo, giturl.filepath);
- });
-}
-
-// Return root of git repo from a filepath
-function resolveGitRoot(filepath) {
- var relativeToGit, repoId;
-
- // No git repo cloned, or file is not in a git repository
- if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null;
-
- // Extract first directory (is the repo id)
- relativeToGit = path.relative(GIT_TMP, filepath);
- repoId = _.first(relativeToGit.split(path.sep));
- if (!repoId) return;
-
- // Return an absolute file
- return path.resolve(GIT_TMP, repoId);
-}
-
-
-module.exports = {
- isUrl: isGitUrl,
- parseUrl: parseGitUrl,
- resolveFile: resolveFileFromGit,
- resolveRoot: resolveGitRoot
};
+
+module.exports = Git;
diff --git a/test/git.js b/test/git.js
index bad031b92..8667b0722 100644
--- a/test/git.js
+++ b/test/git.js
@@ -1,5 +1,8 @@
var should = require('should');
-var git = require('../lib/utils/git');
+var path = require('path');
+var os = require('os');
+
+var Git = require('../lib/utils/git');
describe('Git', function() {
@@ -7,27 +10,27 @@ describe('Git', function() {
it('should correctly validate git urls', function() {
// HTTPS
- git.isUrl('git+https://github.com/Hello/world.git').should.be.ok;
+ Git.isUrl('git+https://github.com/Hello/world.git').should.be.ok;
// SSH
- git.isUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1').should.be.ok;
+ Git.isUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1').should.be.ok;
// Non valid
- git.isUrl('https://github.com/Hello/world.git').should.not.be.ok;
- git.isUrl('README.md').should.not.be.ok;
+ Git.isUrl('https://github.com/Hello/world.git').should.not.be.ok;
+ Git.isUrl('README.md').should.not.be.ok;
});
it('should parse HTTPS urls', function() {
- var parts = git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md');
+ var parts = Git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md');
should.exist(parts);
parts.host.should.be.equal('https://gist.github.com/69ea4542e4c8967d2fa7.git');
- parts.ref.should.be.equal('master');
+ should(parts.ref).be.equal(null);
parts.filepath.should.be.equal('test.md');
});
it('should parse HTTPS urls with a reference', function() {
- var parts = git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#1.0.0');
+ var parts = Git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#1.0.0');
should.exist(parts);
parts.host.should.be.equal('https://gist.github.com/69ea4542e4c8967d2fa7.git');
@@ -36,7 +39,7 @@ describe('Git', function() {
});
it('should parse SSH urls', function() {
- var parts = git.parseUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1');
+ var parts = Git.parseUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1');
should.exist(parts);
parts.host.should.be.equal('git@github.com:GitbookIO/gitbook.git');
@@ -45,4 +48,14 @@ describe('Git', function() {
});
});
+ describe('Cloning and resolving', function() {
+ it('should clone an HTTPS url', function() {
+ var git = new Git(path.join(os.tmpdir(), 'test-git-'+Date.now()));
+ return git.resolve('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md')
+ .then(function(filename) {
+ path.extname(filename).should.equal('.md');
+ });
+ });
+ });
+
});
From 98a13cf487e3b2823cfde38ab1d1b8f97f614517 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 17:30:58 +0100
Subject: [PATCH 031/217] Add conrefs loader for nunjucks
---
lib/book.js | 8 +++++
lib/fs/index.js | 7 +++-
lib/template/index.js | 13 ++++++++
lib/template/loader.js | 75 ++++++++++++++++++++++++++++++++++++++++++
test/all.js | 2 +-
5 files changed, 103 insertions(+), 2 deletions(-)
diff --git a/lib/book.js b/lib/book.js
index f6158338e..a2a11a799 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -287,6 +287,14 @@ Book.prototype.isMultilingual = function() {
return this.langs.count() > 0;
};
+// Return true if file is in the scope of this book
+Book.prototype.isInBook = function(filename) {
+ return pathUtil.isInRoot(
+ this.root,
+ this.resolve(filename)
+ );
+};
+
// Return true if file is in the scope of a child book
Book.prototype.isInLanguageBook = function(filename) {
return _.some(this.langs.list(), function(lang) {
diff --git a/lib/fs/index.js b/lib/fs/index.js
index bcd9345b4..4fd63e9c1 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -2,6 +2,7 @@ var _ = require('lodash');
var path = require('path');
var Buffer = require('buffer').Buffer;
var destroy = require('destroy');
+var os = require('os');
var Promise = require('../utils/promise');
@@ -45,10 +46,14 @@ FS.prototype.readdir = function(folder) {
// To implement for each fs
};
-
// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
// For optmization, it can be redefined:
+// Allocate a new temporary directory
+FS.prototype.tmpdir = function() {
+ return path.resolve(os.tmpdir(), _.uniqueId('gitbook_tmp_'));
+};
+
// List files in a directory
FS.prototype.listFiles = function(folder) {
return this.readdir(folder)
diff --git a/lib/template/index.js b/lib/template/index.js
index 833ff76d7..a3021e622 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -7,6 +7,7 @@ var Promise = require('../utils/promise');
var error = require('../utils/error');
var gitbook = require('../gitbook');
var defaultBlocks = require('./blocks');
+var Loader = require('./loader');
// Return extension name for a specific block
function blockExtName(name) {
@@ -23,6 +24,10 @@ function TemplateEngine(book) {
this.book = book;
this.log = book.log;
+ // Create file loader
+ this.loader = new Loader(this);
+
+ // Create nunjucks instance
this.env = new nunjucks.Environment(
this.loader,
{
@@ -57,6 +62,14 @@ function TemplateEngine(book) {
this.addBlocks(defaultBlocks);
}
+// Interpolate a string content to replace shortcuts according to the filetype
+TemplateEngine.prototype.interpolate = function(filepath, source) {
+ var parser = parsers.get(path.extname(filepath));
+ var type = parser? parser.name : null;
+
+ return this.applyShortcuts(type, source);
+};
+
// Add a new custom filter
TemplateEngine.prototype.addFilter = function(filterName, func) {
try {
diff --git a/lib/template/loader.js b/lib/template/loader.js
index e69de29bb..23ffb7753 100644
--- a/lib/template/loader.js
+++ b/lib/template/loader.js
@@ -0,0 +1,75 @@
+var path = require('path');
+var nunjucks = require('nunjucks');
+
+var Git = require('../utils/git');
+var pathUtil = require('../utils/path');
+
+// The loader should handle relative and git url
+var Loader = nunjucks.Loader.extend({
+ async: true,
+
+ init: function(engine, opts) {
+ this.engine = engine;
+ this.book = engine.book;
+ this.fs = engine.book.fs;
+
+ this.git = new Git(this.fs.tmpdir());
+ },
+
+ getSource: function(soureURL, callback) {
+ var that = this;
+
+ this.git.resolveFile(soureURL)
+ .then(function(filepath) {
+ // Is local file
+ if (!filepath) {
+ filepath = that.book.resolve(soureURL);
+ } else {
+ that.book.log.debug.ln('resolve from git', soureURL, 'to', filepath);
+ }
+
+ // Read file from absolute path
+ return that.fs.readAsString(filepath)
+ .then(function(source) {
+ return that.engine.interpolate(filepath, source);
+ })
+ .then(function(source) {
+ return {
+ src: source,
+ path: filepath,
+
+ // We disable cache since content is modified (shortcuts, ...)
+ noCache: true
+ };
+ });
+ })
+ .nodeify(callback);
+ },
+
+ resolve: function(from, to) {
+ // If origin is in the book, we enforce result file to be in the book
+ if (this.book.isInBook(from)) {
+ return this.book.resolve(
+ this.book.relative(path.dirname(from)),
+ to
+ );
+ }
+
+ // If origin is in a git repository, we resolve file in the git repository
+ var gitRoot = this.git.resolveRoot(from);
+ if (gitRoot) {
+ return pathUtil.resolveInRoot(gitRoot, to);
+ }
+
+ // If origin is not in the book (include from a git content ref)
+ return path.resolve(path.dirname(from), to);
+ },
+
+ // Handle all files as relative, so that nunjucks pass responsability to 'resolve'
+ // Only git urls are considered as absolute
+ isRelative: function(filename) {
+ return !Git.isUrl(filename);
+ }
+});
+
+module.exports = Loader;
diff --git a/test/all.js b/test/all.js
index 0f5560300..827a182b0 100644
--- a/test/all.js
+++ b/test/all.js
@@ -7,8 +7,8 @@ require('./glossary');
require('./langs');
require('./parse');
-require('./template');
require('./git');
+require('./template');
// Output
require('./output-json');
From e7eed2abbe91fa44bd071819123bd9ea04d1702a Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 18:18:33 +0100
Subject: [PATCH 032/217] Complete conrefs in parsing
---
lib/book.js | 2 +-
lib/fs/index.js | 2 +-
lib/template/index.js | 24 ++++++++++++++++++++-
lib/template/loader.js | 8 +++----
lib/utils/error.js | 12 +++++------
lib/utils/git.js | 5 +++++
package.json | 5 ++---
test/mock.js | 1 -
test/template.js | 49 ++++++++++++++++++++++++++++++++++--------
9 files changed, 82 insertions(+), 26 deletions(-)
diff --git a/lib/book.js b/lib/book.js
index a2a11a799..914b48aad 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -291,7 +291,7 @@ Book.prototype.isMultilingual = function() {
Book.prototype.isInBook = function(filename) {
return pathUtil.isInRoot(
this.root,
- this.resolve(filename)
+ filename
);
};
diff --git a/lib/fs/index.js b/lib/fs/index.js
index 4fd63e9c1..3e0d711da 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -51,7 +51,7 @@ FS.prototype.readdir = function(folder) {
// Allocate a new temporary directory
FS.prototype.tmpdir = function() {
- return path.resolve(os.tmpdir(), _.uniqueId('gitbook_tmp_'));
+ return path.resolve(os.tmpdir(), 'gitbook_tmp_'+Date.now());
};
// List files in a directory
diff --git a/lib/template/index.js b/lib/template/index.js
index a3021e622..b59dd0482 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -2,6 +2,7 @@ var _ = require('lodash');
var path = require('path');
var nunjucks = require('nunjucks');
var parsers = require('gitbook-parsers');
+var escapeStringRegexp = require('escape-string-regexp');
var Promise = require('../utils/promise');
var error = require('../utils/error');
@@ -333,7 +334,7 @@ TemplateEngine.prototype.renderString = function(content, context, options) {
}
// Replace shortcuts
- //content = this.applyShortcuts(options.type, content);
+ content = this.applyShortcuts(options.type, content);
return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options)
.fail(function(err) {
@@ -343,5 +344,26 @@ TemplateEngine.prototype.renderString = function(content, context, options) {
});
};
+// Apply a shortcut to a string
+TemplateEngine.prototype.applyShortcut = function(content, shortcut) {
+ var regex = new RegExp(
+ escapeStringRegexp(shortcut.start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(shortcut.end),
+ 'g'
+ );
+ return content.replace(regex, function(all, match) {
+ return '{% '+shortcut.tag.start+' %}'+ match + '{% '+shortcut.tag.end+' %}';
+ });
+};
+
+// Apply all shortcuts to a template
+TemplateEngine.prototype.applyShortcuts = function(type, content) {
+ return _.chain(this.shortcuts)
+ .filter(function(shortcut) {
+ return _.contains(shortcut.parsers, type);
+ })
+ .reduce(this.applyShortcut, content)
+ .value();
+};
+
module.exports = TemplateEngine;
diff --git a/lib/template/loader.js b/lib/template/loader.js
index 23ffb7753..5d30fb26d 100644
--- a/lib/template/loader.js
+++ b/lib/template/loader.js
@@ -16,16 +16,16 @@ var Loader = nunjucks.Loader.extend({
this.git = new Git(this.fs.tmpdir());
},
- getSource: function(soureURL, callback) {
+ getSource: function(sourceURL, callback) {
var that = this;
- this.git.resolveFile(soureURL)
+ this.git.resolve(sourceURL)
.then(function(filepath) {
// Is local file
if (!filepath) {
- filepath = that.book.resolve(soureURL);
+ filepath = that.book.resolve(sourceURL);
} else {
- that.book.log.debug.ln('resolve from git', soureURL, 'to', filepath);
+ that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
}
// Read file from absolute path
diff --git a/lib/utils/error.js b/lib/utils/error.js
index 4ccf83d0b..5ee6a0e0d 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -13,30 +13,30 @@ function enforce(err) {
// Random error wrappers during parsing/generation
var ParsingError = WrappedError({
message: 'Parsing Error: {origMessage}',
- type: 'server.parsing-failed'
+ type: 'parse'
});
var GenerationError = WrappedError({
message: 'Generation Error: {origMessage}',
- type: 'server.parsing-failed'
+ type: 'generate'
});
// Error when output generator does not exists
var GeneratorNotFoundError = TypedError({
- type: 'server.404',
+ type: 'generator.not-found',
message: 'Generator "{generator}" does not exists',
generator: null
});
// A file does not exists
var FileNotFoundError = TypedError({
- type: 'server.404',
+ type: 'file.not-found',
message: 'No "{filename}" file (or is ignored)',
filename: null
});
// A file is outside the scope
var FileOutOfScopeError = TypedError({
- type: 'server.404',
+ type: 'file.out-of-scope',
message: '"{filename}" not in "{root}"',
filename: null,
root: null,
@@ -46,7 +46,7 @@ var FileOutOfScopeError = TypedError({
// Error for nunjucks templates
var TemplateError = WrappedError({
message: 'Error compiling template "{filename}": {origMessage}',
- type: 'client.template-failed',
+ type: 'template',
filename: null
});
diff --git a/lib/utils/git.js b/lib/utils/git.js
index 9fca73ff7..baf0fab1f 100644
--- a/lib/utils/git.js
+++ b/lib/utils/git.js
@@ -51,6 +51,11 @@ Git.prototype.clone = function(host, ref) {
// Get file from a git repo
Git.prototype.resolve = function(giturl) {
+ // Path to a file in a git repo?
+ if (!Git.isUrl(giturl)) {
+ if (this.resolveRoot(giturl)) return Promise(giturl);
+ return Promise(null);
+ }
if (_.isString(giturl)) giturl = Git.parseUrl(giturl);
if (!giturl) return Promise(null);
diff --git a/package.json b/package.json
index fa333fb09..7dce193a5 100644
--- a/package.json
+++ b/package.json
@@ -47,9 +47,8 @@
},
"devDependencies": {
"eslint": "1.5.0",
- "mocha": "2.3.2",
- "should": "7.1.0",
- "should-promised": "0.3.1",
+ "mocha": "2.4.5",
+ "should": "8.2.2",
"gulp": "^3.8.11",
"gulp-rename": "^1.2.2",
"gulp-uglify": "1.1.0",
diff --git a/test/mock.js b/test/mock.js
index 6d53ed842..1f7e7d5bf 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -5,7 +5,6 @@ var tmp = require('tmp');
var path = require('path');
var should = require('should');
-require('should-promised');
var Book = require('../').Book;
var Output = require('../lib/output');
diff --git a/test/template.js b/test/template.js
index b55693f62..f338a848e 100644
--- a/test/template.js
+++ b/test/template.js
@@ -1,32 +1,63 @@
var mock = require('./mock');
-var TemplateEngine = require('../lib/template');
-
var pkg = require('../package.json');
describe('Template', function() {
- var book, tpl;
+ var book;
before(function() {
- return mock.setupDefaultBook()
+ return mock.setupDefaultBook({
+ 'test.md': 'World'
+ })
.then(function(_book) {
book = _book;
return book.parse();
- })
- .then(function() {
- tpl = new TemplateEngine(book);
});
});
describe('.renderString', function() {
it('should render a simple string', function() {
- return tpl.renderString('Hello World')
+ return book.template.renderString('Hello World')
.should.be.fulfilledWith('Hello World');
});
it('should render with variable', function() {
- return tpl.renderString('Version is {{ gitbook.version }}')
+ return book.template.renderString('Version is {{ gitbook.version }}')
.should.be.fulfilledWith('Version is '+pkg.version);
});
});
+ describe('Conrefs Loader', function() {
+ it('should include a local file', function() {
+ return book.template.renderString('Hello {% include "./test.md" %}')
+ .should.be.fulfilledWith('Hello World');
+ });
+
+ it('should include a git url', function() {
+ return book.template.renderString('Hello {% include "./test.md" %}')
+ .should.be.fulfilledWith('Hello World');
+ });
+
+ it('should reject file out of scope', function() {
+ return book.template.renderString('Hello {% include "../test.md" %}')
+ .should.be.rejected();
+ });
+
+ describe('Git Urls', function() {
+ it('should include a file from a git repo', function() {
+ return book.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}')
+ .should.be.fulfilledWith('Hello from git');
+ });
+
+ it('should handle deep inclusion (1)', function() {
+ return book.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}')
+ .should.be.fulfilledWith('First Hello. Hello from git');
+ });
+
+ it('should handle deep inclusion (2)', function() {
+ return book.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}')
+ .should.be.fulfilledWith('First Hello. Hello from git');
+ });
+ });
+ });
+
});
From 669f3b39849890c48171d807225cd6eaa3c9086b Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 21:44:38 +0100
Subject: [PATCH 033/217] Add base for normalizing html
---
lib/book.js | 2 +-
.../{website.js => website/index.js} | 12 ++--
lib/generators/website/theme.js | 6 ++
lib/page/html.js | 57 +++++++++++++++++++
lib/{backbone/page.js => page/index.js} | 23 +++++++-
lib/utils/command.js | 27 ++++++++-
lib/utils/images.js | 27 +++++++++
lib/utils/promise.js | 12 ++++
8 files changed, 156 insertions(+), 10 deletions(-)
rename lib/generators/{website.js => website/index.js} (66%)
create mode 100644 lib/generators/website/theme.js
create mode 100644 lib/page/html.js
rename lib/{backbone/page.js => page/index.js} (80%)
create mode 100644 lib/utils/images.js
diff --git a/lib/book.js b/lib/book.js
index 914b48aad..0f73135dc 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -9,7 +9,7 @@ var Readme = require('./backbone/readme');
var Glossary = require('./backbone/glossary');
var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
-var Page = require('./backbone/page');
+var Page = require('./page');
var TemplateEngine = require('./template');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
diff --git a/lib/generators/website.js b/lib/generators/website/index.js
similarity index 66%
rename from lib/generators/website.js
rename to lib/generators/website/index.js
index a2c33113d..f474cbb89 100644
--- a/lib/generators/website.js
+++ b/lib/generators/website/index.js
@@ -1,5 +1,5 @@
var util = require('util');
-var Generator = require('./base');
+var Generator = require('../base');
function WebsiteGenerator() {
Generator.apply(this, arguments);
@@ -8,12 +8,10 @@ util.inherits(WebsiteGenerator, Generator);
// Copy an asset file
WebsiteGenerator.prototype.writeAsset = function(filename) {
- var that = this;
-
- return that.book.readFile(filename)
- .then(function(buf) {
- return that.output.writeFile(filename, buf);
- });
+ return this.output.copyFile(
+ this.book.resolve(filename),
+ filename
+ );
};
// Write a page (parsable file)
diff --git a/lib/generators/website/theme.js b/lib/generators/website/theme.js
new file mode 100644
index 000000000..1cc2891ae
--- /dev/null
+++ b/lib/generators/website/theme.js
@@ -0,0 +1,6 @@
+
+function Theme() {
+
+}
+
+module.exports = Theme;
diff --git a/lib/page/html.js b/lib/page/html.js
new file mode 100644
index 000000000..f828d11ef
--- /dev/null
+++ b/lib/page/html.js
@@ -0,0 +1,57 @@
+var _ = require('lodash');
+var cheerio = require('cheerio');
+var domSerializer = require('dom-serializer');
+var slug = require('github-slugid');
+
+var Promise = require('../utils/promise');
+
+// Render a cheerio DOM as html
+function renderDOM($, dom, options) {
+ if (!dom && $._root && $._root.children) {
+ dom = $._root.children;
+ }
+ options = options|| dom.options || $._options;
+ return domSerializer(dom, options);
+}
+
+function HTMLPipeline(htmlString, opts) {
+ _.bindAll(this);
+
+ this.opts = _.defaults(opts || {}, {
+ convertImages: true
+ });
+
+ this.$ = cheerio.load(htmlString, {
+ // We should parse html without trying to normalize too much
+ xmlMode: false,
+
+ // SVG need some attributes to use uppercases
+ lowerCaseAttributeNames: false,
+ lowerCaseTags: false
+ });
+}
+
+// Add ID to headings
+HTMLPipeline.prototype.addHeadingIDs = function() {
+ var that = this;
+
+ this.$('h1,h2,h3,h4,h5,h6').each(function() {
+ // Already has an ID?
+ if (that.$(this).attr('id')) return;
+
+ that.$(this).attr('id', slug(that.$(this).text()));
+ });
+};
+
+// Write content to the pipeline
+HTMLPipeline.prototype.output = function() {
+ var that = this;
+
+ return Promise()
+ .then(this.addHeadingIDs)
+ .then(function() {
+ return renderDOM(that.$);
+ });
+};
+
+module.exports = HTMLPipeline;
diff --git a/lib/backbone/page.js b/lib/page/index.js
similarity index 80%
rename from lib/backbone/page.js
rename to lib/page/index.js
index 94812d991..8f8819cf6 100644
--- a/lib/backbone/page.js
+++ b/lib/page/index.js
@@ -3,6 +3,8 @@ var path = require('path');
var parsers = require('gitbook-parsers');
var error = require('../utils/error');
+var Promise = require('../utils/promise');
+var HTMLPipeline = require('./html');
/*
A page represent a parsable file in the book (Markdown, Asciidoc, etc)
@@ -63,9 +65,14 @@ Page.prototype.read = function() {
};
// Parse the page and return its content
-Page.prototype.parse = function() {
+Page.prototype.parse = function(opts) {
var that = this;
+ opts = _.defaults(opts || {}, {
+
+ });
+
+
this.log.debug.ln('start parsing file', this.path);
return this.read()
@@ -93,6 +100,20 @@ Page.prototype.parse = function() {
.then(function() {
return that.parser.page(that.content)
.then(that.update);
+ })
+
+ // Normalize HTML output
+ .then(function() {
+ return Promise.map(that.content.sections, function(section) {
+ var pipeline = new HTMLPipeline(section.content, opts);
+
+ return pipeline.output()
+ .then(function(content) {
+ return {
+ content: content
+ };
+ });
+ });
});
};
diff --git a/lib/utils/command.js b/lib/utils/command.js
index f395fa1d1..4269d6cfb 100644
--- a/lib/utils/command.js
+++ b/lib/utils/command.js
@@ -13,7 +13,32 @@ function exec(command, options) {
return Promise.nfcall(childProcess.exec, command, options);
}
+// Spawn an executable
+function spawn(command, args, options) {
+ if (!isAvailable) {
+ return Promise.reject(new Error('Command execution is not possible on this platform'));
+ }
+
+ var d = Promise.deferred();
+ var child = childProcess.spawn(command, args, options);
+
+ child.on('error', function(error) {
+ return d.reject(error);
+ });
+
+ child.on('close', function(code) {
+ if (code === 0) {
+ d.resolve();
+ } else {
+ d.reject(new Error('Error with command "'+command+'"'));
+ }
+ });
+
+ return d.promise;
+}
+
module.exports = {
isAvailable: isAvailable,
- exec: exec
+ exec: exec,
+ spawn: spawn
};
diff --git a/lib/utils/images.js b/lib/utils/images.js
new file mode 100644
index 000000000..3ba0f1f06
--- /dev/null
+++ b/lib/utils/images.js
@@ -0,0 +1,27 @@
+var fs = require('fs');
+
+var Promise = require('./promise');
+var command = require('./command');
+var error = require('./error');
+
+// Convert a svg file to a pmg
+function convertSVGToPNG(source, dest, options) {
+ if (!command.isAvailable) return Promise.reject(new Error('Could not convert SVG in this platform'));
+ if (!fs.existsSync(source)) return Promise.reject(new error.FileNotFoundError({ filename: source }));
+
+ return command.spawn('svgexport', [source, dest])
+ .fail(function(err) {
+ if (err.code == 'ENOENT') err = new Error('Need to install "svgexport" using "npm install svgexport -g"');
+ throw err;
+ })
+ .then(function() {
+ if (fs.existsSync(dest)) return;
+
+ throw new Error('Error converting '+source+' into '+dest);
+ });
+}
+
+module.exports = {
+ convertSVGToPNG: convertSVGToPNG,
+ INVALID: ['.svg']
+};
\ No newline at end of file
diff --git a/lib/utils/promise.js b/lib/utils/promise.js
index 82f4a6006..adcc1c3d4 100644
--- a/lib/utils/promise.js
+++ b/lib/utils/promise.js
@@ -32,7 +32,19 @@ function some(arr, iter) {
}, Q());
}
+// Map an array using an async (promised) iterator
+function map(arr, iter) {
+ return reduce(arr, function(prev, entry, i) {
+ return Q(iter(entry, i))
+ .then(function(out) {
+ prev.push(out);
+ return prev;
+ });
+ }, []);
+}
+
module.exports = Q;
module.exports.reduce = reduce;
+module.exports.map = map;
module.exports.serie = serie;
module.exports.some = some;
From 756694c029218510592418deb8aaf6f3b36f95c3 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 11 Feb 2016 22:12:07 +0100
Subject: [PATCH 034/217] Page output a simple html string
---
lib/book.js | 1 +
lib/generators/json.js | 2 +-
lib/generators/website/index.js | 3 --
lib/page/html.js | 36 ++++++++++++++++---
lib/page/index.js | 23 ++++++------
lib/utils/location.js | 14 ++++++--
test/all.js | 1 +
test/assertions.js | 62 +++++++++++++++++++++++++++++++++
test/mock.js | 18 ++--------
test/page.js | 26 ++++++++++++++
10 files changed, 149 insertions(+), 37 deletions(-)
create mode 100644 test/assertions.js
create mode 100644 test/page.js
diff --git a/lib/book.js b/lib/book.js
index 0f73135dc..148706cca 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -214,6 +214,7 @@ Book.prototype.addPage = function(filename) {
if (this.pages[filename]) return;
this.pages[filename] = new Page(this, filename);
+ return this.pages[filename];
};
// Return a page by its filename (or undefined)
diff --git a/lib/generators/json.js b/lib/generators/json.js
index 560f099dd..5ba2d1676 100644
--- a/lib/generators/json.js
+++ b/lib/generators/json.js
@@ -21,7 +21,7 @@ JSONGenerator.prototype.writePage = function(page) {
version: gitbook.version
},
path: page.path,
- sections: page.content.sections
+ sections: page.content
};
return that.output.writeFile(
diff --git a/lib/generators/website/index.js b/lib/generators/website/index.js
index f474cbb89..67c80b628 100644
--- a/lib/generators/website/index.js
+++ b/lib/generators/website/index.js
@@ -19,7 +19,4 @@ WebsiteGenerator.prototype.writePage = function(page) {
};
-
-
-
module.exports = WebsiteGenerator;
diff --git a/lib/page/html.js b/lib/page/html.js
index f828d11ef..b19d5ede5 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -4,6 +4,7 @@ var domSerializer = require('dom-serializer');
var slug = require('github-slugid');
var Promise = require('../utils/promise');
+var location = require('../utils/location');
// Render a cheerio DOM as html
function renderDOM($, dom, options) {
@@ -18,7 +19,10 @@ function HTMLPipeline(htmlString, opts) {
_.bindAll(this);
this.opts = _.defaults(opts || {}, {
- convertImages: true
+ convertImages: true,
+
+ // Calcul new href for a relative link
+ onRelativeLink: _.identity
});
this.$ = cheerio.load(htmlString, {
@@ -31,15 +35,38 @@ function HTMLPipeline(htmlString, opts) {
});
}
+// Normalize links
+HTMLPipeline.prototype.normalizeLinks = function() {
+ var that = this;
+
+ this.$('a').each(function() {
+ var $a = that.$(this);
+
+ var href = $a.attr('href');
+ if (!href) return;
+
+ if (location.isAnchor(href)) {
+ // Don't "change" anchor links
+ } else if (location.isRelative(href)) {
+ $a.attr('href', that.opts.onRelativeLink(href));
+ } else {
+ // External links
+ $a.attr('target', '_blank');
+ }
+
+ });
+};
+
// Add ID to headings
HTMLPipeline.prototype.addHeadingIDs = function() {
var that = this;
this.$('h1,h2,h3,h4,h5,h6').each(function() {
- // Already has an ID?
- if (that.$(this).attr('id')) return;
+ var $h = that.$(this);
- that.$(this).attr('id', slug(that.$(this).text()));
+ // Already has an ID?
+ if ($h.attr('id')) return;
+ $h.attr('id', slug($h.text()));
});
};
@@ -48,6 +75,7 @@ HTMLPipeline.prototype.output = function() {
var that = this;
return Promise()
+ .then(this.normalizeLinks)
.then(this.addHeadingIDs)
.then(function() {
return renderDOM(that.$);
diff --git a/lib/page/index.js b/lib/page/index.js
index 8f8819cf6..e7a4fec19 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -3,7 +3,6 @@ var path = require('path');
var parsers = require('gitbook-parsers');
var error = require('../utils/error');
-var Promise = require('../utils/promise');
var HTMLPipeline = require('./html');
/*
@@ -99,21 +98,23 @@ Page.prototype.parse = function(opts) {
// Render markup using the parser
.then(function() {
return that.parser.page(that.content)
- .then(that.update);
+ .then(function(out) {
+ var content = _.pluck(out.sections, 'content').join('\n');
+ that.update(content);
+ });
})
// Normalize HTML output
.then(function() {
- return Promise.map(that.content.sections, function(section) {
- var pipeline = new HTMLPipeline(section.content, opts);
+ var pipelineOpts = _.extend({
+ onRelativeLink: function(href) {
+ console.log('href', href);
+ }
+ }, opts);
+ var pipeline = new HTMLPipeline(that.content, pipelineOpts);
- return pipeline.output()
- .then(function(content) {
- return {
- content: content
- };
- });
- });
+ return pipeline.output()
+ .then(that.update);
});
};
diff --git a/lib/utils/location.js b/lib/utils/location.js
index d57e84fb4..efe1425a1 100644
--- a/lib/utils/location.js
+++ b/lib/utils/location.js
@@ -9,13 +9,23 @@ function isExternal(href) {
}
}
-
// Inverse of isExternal
function isRelative(href) {
return !isExternal(href);
}
+// Return true if the link is an achor
+function isAnchor(href) {
+ try {
+ var parsed = url.parse(href);
+ return !!(!parsed.protocol && !parsed.path && parsed.hash);
+ } catch(err) {
+ return false;
+ }
+}
+
module.exports = {
isExternal: isExternal,
- isRelative: isRelative
+ isRelative: isRelative,
+ isAnchor: isAnchor
};
diff --git a/test/all.js b/test/all.js
index 827a182b0..08be793ac 100644
--- a/test/all.js
+++ b/test/all.js
@@ -5,6 +5,7 @@ require('./readme');
require('./summary');
require('./glossary');
require('./langs');
+require('./page');
require('./parse');
require('./git');
diff --git a/test/assertions.js b/test/assertions.js
new file mode 100644
index 000000000..b9369953d
--- /dev/null
+++ b/test/assertions.js
@@ -0,0 +1,62 @@
+var fs = require('fs');
+var _ = require('lodash');
+var cheerio = require('cheerio');
+var should = require('should');
+
+// Assertions to test if an Output has generated a file
+should.Assertion.add('file', function(file, description) {
+ this.params = {
+ actual: this.obj.toString(),
+ operator: 'have file ' + file,
+ message: description
+ };
+
+ this.obj.should.have.property('resolve').which.is.a.Function;
+ this.assert(fs.existsSync(this.obj.resolve(file)));
+});
+
+should.Assertion.add('html', function(rules, description) {
+ this.params = { actual: 'HTML string', operator: 'valid html', message: description };
+ var $ = cheerio.load(this.obj);
+
+ _.each(rules, function(validations, query) {
+ validations = _.defaults(validations || {}, {
+ // Select a specific element in the list of matched elements
+ index: null,
+
+ // Check that there is the correct count of elements
+ count: 1,
+
+ // Check attribute values
+ attributes: {},
+
+ // Trim inner text
+ trim: false,
+
+ // Check inner text
+ text: undefined
+ });
+
+ var $el = $(query);
+
+ // Select correct element
+ if (_.isNumber(validations.index)) $el = $($el.get(validations.index));
+
+ // Test number of elements
+ $el.length.should.be.equal(validations.count);
+
+ // Test text
+ if (validations.text !== undefined) {
+ var text = $el.text();
+ if (validations.trim) text = text.trim();
+ text.should.be.equal(validations.text);
+ }
+
+ // Test attributes
+ _.each(validations.attributes, function(value, name) {
+ var attr = $el.attr(name);
+ should(attr).be.ok();
+ attr.should.be.equal(value);
+ });
+ });
+});
diff --git a/test/mock.js b/test/mock.js
index 1f7e7d5bf..ce7b02775 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -1,15 +1,14 @@
var Q = require('q');
var _ = require('lodash');
-var fs = require('fs');
var tmp = require('tmp');
var path = require('path');
-var should = require('should');
-
var Book = require('../').Book;
var Output = require('../lib/output');
var NodeFS = require('../lib/fs/node');
+require('./assertions');
+
// Create filesystem instance for testing
var nodeFS = new NodeFS();
@@ -74,19 +73,6 @@ function outputDefaultBook(generator, files, opts) {
});
}
-// Assertions to test if an Output has generated a file
-should.Assertion.add('file', function(file, description) {
- this.params = {
- actual: this.obj.toString(),
- operator: 'have file ' + file,
- message: description
- };
-
- this.obj.should.have.property('resolve').which.is.a.Function;
- this.assert(fs.existsSync(this.obj.resolve(file)));
-});
-
-
module.exports = {
setupBook: setupBook,
setupDefaultBook: setupDefaultBook,
diff --git a/test/page.js b/test/page.js
new file mode 100644
index 000000000..66c06e1b2
--- /dev/null
+++ b/test/page.js
@@ -0,0 +1,26 @@
+var mock = require('./mock');
+
+describe('Page', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'heading.md': '# Hello\n\n## World'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.summary.load();
+ });
+ });
+
+ it.only('should add a default ID to headings', function() {
+ var page = book.addPage('heading.md');
+
+ return page.parse()
+ .then(function() {
+ console.log(page.content);
+ });
+
+ });
+
+});
\ No newline at end of file
From a853a51a0ba0976e8e28f368bd88717611b3c477 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Fri, 12 Feb 2016 18:53:00 +0100
Subject: [PATCH 035/217] Complete test for heading IDs
---
test/page.js | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/test/page.js b/test/page.js
index 66c06e1b2..5f5c31185 100644
--- a/test/page.js
+++ b/test/page.js
@@ -13,12 +13,19 @@ describe('Page', function() {
});
});
- it.only('should add a default ID to headings', function() {
+ it('should add a default ID to headings', function() {
var page = book.addPage('heading.md');
return page.parse()
.then(function() {
- console.log(page.content);
+ page.content.should.be.html({
+ 'h1#hello': {
+ count: 1
+ },
+ 'h2#world': {
+ count: 1
+ }
+ });
});
});
From 4555c541a8f98cb6ad4cbec2d7bf85b375dbf505 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Fri, 12 Feb 2016 19:20:32 +0100
Subject: [PATCH 036/217] Replace links to page of summary by html links
---
lib/backbone/file.js | 14 ++++++------
lib/backbone/summary.js | 4 ++--
lib/book.js | 6 +++---
lib/config/index.js | 6 +++---
lib/output.js | 2 +-
lib/page/index.js | 31 ++++++++++++++++++++++-----
lib/utils/path.js | 11 +++++++++-
test/page.js | 47 ++++++++++++++++++++++++++++++++---------
8 files changed, 89 insertions(+), 32 deletions(-)
diff --git a/lib/backbone/file.js b/lib/backbone/file.js
index a58063cf0..6c46d5e7e 100644
--- a/lib/backbone/file.js
+++ b/lib/backbone/file.js
@@ -7,7 +7,7 @@ function BackboneFile(book) {
this.log = this.book.log;
// Filename in the book
- this.filename = '';
+ this.path = '';
this.parser;
_.bindAll(this);
@@ -23,7 +23,7 @@ BackboneFile.prototype.parse = function() {
// Return true if backbone file exists
BackboneFile.prototype.exists = function() {
- return Boolean(this.filename);
+ return Boolean(this.path);
};
// Locate a backbone file, could be .md, .asciidoc, etc
@@ -36,7 +36,7 @@ BackboneFile.prototype.locate = function() {
.then(function(result) {
if (!result) return;
- that.filename = result.path;
+ that.path = result.path;
that.parser = result.parser;
});
};
@@ -44,15 +44,15 @@ BackboneFile.prototype.locate = function() {
// Read and parse the file
BackboneFile.prototype.load = function() {
var that = this;
- this.log.debug.ln('loading', this.type, ':', that.filename);
+ this.log.debug.ln('loading', this.type, ':', that.path);
return this.locate()
.then(function() {
- if (!that.filename) return;
+ if (!that.path) return;
- that.log.debug.ln(that.type, 'located at', that.filename);
+ that.log.debug.ln(that.type, 'located at', that.path);
- return that.book.readFile(that.filename)
+ return that.book.readFile(that.path)
// Parse it
.then(function(content) {
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 35f65e6f5..aa521041e 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -23,7 +23,7 @@ function TOCArticle(summary, title, ref, articles, parent) {
}
var parts = url.parse(ref);
- this.filename = parts.pathname;
+ this.path = parts.pathname;
this.anchor = parts.hash;
this.articles = _.map(articles || [], function(article) {
@@ -42,7 +42,7 @@ TOCArticle.prototype.walk = function(iter) {
// Return true if is pointing to a file
TOCArticle.prototype.hasLocation = function() {
- return Boolean(this.filename);
+ return Boolean(this.path);
};
// Return true if has children
diff --git a/lib/book.js b/lib/book.js
index 148706cca..8c0d39b8d 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -199,7 +199,7 @@ Book.prototype.parse = function() {
// Index summary's articles
that.summary.walk(function(article) {
if (!article.hasLocation()) return;
- that.addPage(article.filename);
+ that.addPage(article.path);
});
})
@@ -210,9 +210,9 @@ Book.prototype.parse = function() {
// Mark a filename as being parsable
Book.prototype.addPage = function(filename) {
- filename = pathUtil.normalize(filename);
+ if (this.hasPage(filename)) return this.getPage(filename);
- if (this.pages[filename]) return;
+ filename = pathUtil.normalize(filename);
this.pages[filename] = new Page(this, filename);
return this.pages[filename];
};
diff --git a/lib/config/index.js b/lib/config/index.js
index 563f03bd8..d83dae479 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -19,7 +19,7 @@ function Config(book, baseConfig) {
this.book = book;
this.fs = book.fs;
this.log = book.log;
- this.filename = null;
+ this.path = null;
this.replace(baseConfig || {});
}
@@ -37,7 +37,7 @@ Config.prototype.load = function() {
return that.fs.loadAsObject(that.book.resolve(filename))
.then(function(_config) {
- that.filename = filename;
+ that.path = filename;
return that.replace(_config);
})
.fail(function(err) {
@@ -102,7 +102,7 @@ Config.prototype.replace = function(options) {
// Return true if book has a configuration file
Config.prototype.exists = function() {
- return Boolean(this.filename);
+ return Boolean(this.path);
};
// Return path to a structure file
diff --git a/lib/output.js b/lib/output.js
index b787edc78..2ada022da 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -27,7 +27,7 @@ function Output(book, type) {
'.bookignore',
// The configuration file should not be copied in the output
- this.book.config.filename
+ this.book.config.path
]));
}
diff --git a/lib/page/index.js b/lib/page/index.js
index e7a4fec19..711e0e1b2 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -3,6 +3,7 @@ var path = require('path');
var parsers = require('gitbook-parsers');
var error = require('../utils/error');
+var pathUtil = require('../utils/path');
var HTMLPipeline = require('./html');
/*
@@ -40,10 +41,25 @@ function Page(book, filename) {
// Return the filename of the page with another extension
// "README.md" -> "README.html"
Page.prototype.withExtension = function(ext) {
- return path.join(
- path.dirname(this.path),
- path.basename(this.path, path.extname(this.path)) + ext
- );
+ return pathUtil.setExtension(this.path, ext);
+};
+
+// Filename for output
+// READMEs are replaced by index.html
+Page.prototype.outputPath = function(ext) {
+ ext = ext || '.html';
+ var output;
+
+ if (
+ path.basename(this.path, path.extname(this.path)) == 'README' ||
+ output == this.book.readme.path
+ ) {
+ output = path.join(path.dirname(output), 'index'+ext);
+ } else {
+ output = pathUtil.setExtension(output, ext);
+ }
+
+ return output;
};
// Update content of the page
@@ -107,8 +123,13 @@ Page.prototype.parse = function(opts) {
// Normalize HTML output
.then(function() {
var pipelineOpts = _.extend({
+
+ // Replace links to page of summary
onRelativeLink: function(href) {
- console.log('href', href);
+ var to = that.book.getPage(href);
+ if (to) return to.outputPath();
+
+ return href;
}
}, opts);
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
diff --git a/lib/utils/path.js b/lib/utils/path.js
index f195d6c29..c233c923f 100644
--- a/lib/utils/path.js
+++ b/lib/utils/path.js
@@ -42,8 +42,17 @@ function resolveInRoot(root) {
return result;
}
+// Chnage extension
+function setExtension(filename, ext) {
+ return path.join(
+ path.dirname(filename),
+ path.basename(filename, path.extname(filename)) + ext
+ );
+}
+
module.exports = {
isInRoot: isInRoot,
resolveInRoot: resolveInRoot,
- normalize: normalizePath
+ normalize: normalizePath,
+ setExtension: setExtension
};
diff --git a/test/page.js b/test/page.js
index 5f5c31185..90a02602a 100644
--- a/test/page.js
+++ b/test/page.js
@@ -5,29 +5,56 @@ describe('Page', function() {
before(function() {
return mock.setupDefaultBook({
- 'heading.md': '# Hello\n\n## World'
+ 'heading.md': '# Hello\n\n## World',
+ 'links.md': '[link](hello.md) [readme](README.md)'
})
.then(function(_book) {
book = _book;
- return book.summary.load();
+ return book.parse();
});
});
- it('should add a default ID to headings', function() {
- var page = book.addPage('heading.md');
+ describe('Headings', function() {
+ it('should add a default ID to headings', function() {
+ var page = book.addPage('heading.md');
- return page.parse()
- .then(function() {
+ return page.parse()
+ .then(function() {
+ page.content.should.be.html({
+ 'h1#hello': {
+ count: 1
+ },
+ 'h2#world': {
+ count: 1
+ }
+ });
+ });
+ });
+ });
+
+ describe('Links', function() {
+ var page;
+
+ before(function() {
+ page = book.addPage('links.md');
+ return page.parse();
+ });
+
+ it('should replace links to page to .html', function() {
page.content.should.be.html({
- 'h1#hello': {
- count: 1
- },
- 'h2#world': {
+ 'a[href="index.html"]': {
count: 1
}
});
});
+ it('should not replace links to file not in SUMMARY', function() {
+ page.content.should.be.html({
+ 'a[href="hello.md"]': {
+ count: 1
+ }
+ });
+ });
});
});
\ No newline at end of file
From 82f94b010f1d77957c9d1b0967dcdd5eafe73c39 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Fri, 12 Feb 2016 20:48:51 +0100
Subject: [PATCH 037/217] Remove concept of generator, to merge it with Output
---
lib/generators/base.js | 29 --------------
lib/generators/website/index.js | 22 -----------
lib/{output.js => output/base.js} | 42 ++++++++++++++-------
lib/{generators => output}/index.js | 0
lib/{generators => output}/json.js | 20 +++++-----
lib/output/website/index.js | 22 +++++++++++
lib/{generators => output}/website/theme.js | 0
lib/page/index.js | 5 ++-
lib/utils/error.js | 14 ++-----
test/mock.js | 5 +--
test/output-json.js | 3 +-
11 files changed, 71 insertions(+), 91 deletions(-)
delete mode 100644 lib/generators/base.js
delete mode 100644 lib/generators/website/index.js
rename lib/{output.js => output/base.js} (79%)
rename lib/{generators => output}/index.js (100%)
rename lib/{generators => output}/json.js (64%)
create mode 100644 lib/output/website/index.js
rename lib/{generators => output}/website/theme.js (100%)
diff --git a/lib/generators/base.js b/lib/generators/base.js
deleted file mode 100644
index 0fc0be91a..000000000
--- a/lib/generators/base.js
+++ /dev/null
@@ -1,29 +0,0 @@
-
-function Generator(output, type) {
- this.output = output;
- this.book = output.book;
- this.type = type;
-}
-
-// Prepare the generation
-Generator.prototype.prepare = function() {
-
-};
-
-// Copy an asset file (non-parsable), ex: images, etc
-Generator.prototype.writeAsset = function(filename) {
-
-};
-
-// Write a page (parsable file), ex: markdown, etc
-Generator.prototype.writePage = function(page) {
-
-};
-
-// Finish the generation
-Generator.prototype.finish = function() {
-
-};
-
-
-module.exports = Generator;
diff --git a/lib/generators/website/index.js b/lib/generators/website/index.js
deleted file mode 100644
index 67c80b628..000000000
--- a/lib/generators/website/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-var util = require('util');
-var Generator = require('../base');
-
-function WebsiteGenerator() {
- Generator.apply(this, arguments);
-}
-util.inherits(WebsiteGenerator, Generator);
-
-// Copy an asset file
-WebsiteGenerator.prototype.writeAsset = function(filename) {
- return this.output.copyFile(
- this.book.resolve(filename),
- filename
- );
-};
-
-// Write a page (parsable file)
-WebsiteGenerator.prototype.writePage = function(page) {
-
-};
-
-module.exports = WebsiteGenerator;
diff --git a/lib/output.js b/lib/output/base.js
similarity index 79%
rename from lib/output.js
rename to lib/output/base.js
index 2ada022da..8bc27d428 100644
--- a/lib/output.js
+++ b/lib/output/base.js
@@ -2,22 +2,17 @@ var _ = require('lodash');
var Ignore = require('ignore');
var path = require('path');
-var Promise = require('./utils/promise');
-var pathUtil = require('./utils/path');
-var error = require('./utils/error');
-var fs = require('./utils/fs');
-var generators = require('./generators');
-var PluginsManager = require('./plugins');
+var Promise = require('../utils/promise');
+var pathUtil = require('../utils/path');
+var fs = require('../utils/fs');
+var PluginsManager = require('../plugins');
function Output(book, type) {
- if (!generators[type]) throw error.GeneratorNotFoundError({ generator: type });
-
this.book = book;
this.log = this.book.log;
this.type = type;
this.plugins = new PluginsManager(book);
- this.generator = new generators[type](this, type);
// Files to ignore in output
this.ignore = Ignore();
@@ -95,7 +90,7 @@ Output.prototype.generate = function() {
// Initialize the generation
.then(function() {
- return that.generator.prepare();
+ return that.prepare();
})
// Process all files
@@ -109,17 +104,38 @@ Output.prototype.generate = function() {
// Process file as page or asset
if (that.book.hasPage(filename)) {
- return that.generator.writePage(that.book.getPage(filename));
+ return that.writePage(that.book.getPage(filename));
} else {
- return that.generator.writeAsset(filename);
+ return that.copyAsset(filename);
}
});
})
// Finish the generation
.then(function() {
- return that.generator.finish();
+ return that.finish();
});
};
+// Prepare the generation
+Output.prototype.prepare = function() {
+
+};
+
+// Copy an asset file (non-parsable), ex: images, etc
+Output.prototype.copyAsset = function(filename) {
+
+};
+
+// Write a page (parsable file), ex: markdown, etc
+Output.prototype.writePage = function(page) {
+
+};
+
+// Finish the generation
+Output.prototype.finish = function() {
+
+};
+
+
module.exports = Output;
diff --git a/lib/generators/index.js b/lib/output/index.js
similarity index 100%
rename from lib/generators/index.js
rename to lib/output/index.js
diff --git a/lib/generators/json.js b/lib/output/json.js
similarity index 64%
rename from lib/generators/json.js
rename to lib/output/json.js
index 5ba2d1676..b03df6e21 100644
--- a/lib/generators/json.js
+++ b/lib/output/json.js
@@ -1,14 +1,14 @@
var util = require('util');
-var Generator = require('./base');
+var Output = require('./base');
var gitbook = require('../gitbook');
-function JSONGenerator() {
- Generator.apply(this, arguments);
+function JSONOutput() {
+ Output.apply(this, arguments);
}
-util.inherits(JSONGenerator, Generator);
+util.inherits(JSONOutput, Output);
// Write a page (parsable file)
-JSONGenerator.prototype.writePage = function(page) {
+JSONOutput.prototype.writePage = function(page) {
var that = this;
// Parse the page
@@ -24,7 +24,7 @@ JSONGenerator.prototype.writePage = function(page) {
sections: page.content
};
- return that.output.writeFile(
+ return that.writeFile(
page.withExtension('.json'),
JSON.stringify(json, null, 4)
);
@@ -32,16 +32,16 @@ JSONGenerator.prototype.writePage = function(page) {
};
// At the end of generation, generate README.json for multilingual books
-JSONGenerator.prototype.finish = function() {
+JSONOutput.prototype.finish = function() {
if (!this.book.isMultilingual()) return;
// Copy README.json from main book
var mainLanguage = this.book.langs.getDefault().id;
- return this.output.copyFile(
- this.output.resolve(mainLanguage, 'README.json'),
+ return this.copyFile(
+ this.resolve(mainLanguage, 'README.json'),
'README.json'
);
};
-module.exports = JSONGenerator;
+module.exports = JSONOutput;
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
new file mode 100644
index 000000000..2b7db7341
--- /dev/null
+++ b/lib/output/website/index.js
@@ -0,0 +1,22 @@
+var util = require('util');
+var Output = require('../base');
+
+function WebsiteOutput() {
+ Output.apply(this, arguments);
+}
+util.inherits(WebsiteOutput, Output);
+
+// Copy an asset file
+WebsiteOutput.prototype.writeAsset = function(filename) {
+ return this.copyFile(
+ this.book.resolve(filename),
+ filename
+ );
+};
+
+// Write a page (parsable file)
+WebsiteOutput.prototype.writePage = function(page) {
+
+};
+
+module.exports = WebsiteOutput;
diff --git a/lib/generators/website/theme.js b/lib/output/website/theme.js
similarity index 100%
rename from lib/generators/website/theme.js
rename to lib/output/website/theme.js
diff --git a/lib/page/index.js b/lib/page/index.js
index 711e0e1b2..707478077 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -84,7 +84,7 @@ Page.prototype.parse = function(opts) {
var that = this;
opts = _.defaults(opts || {}, {
-
+ linkPages: true
});
@@ -123,9 +123,10 @@ Page.prototype.parse = function(opts) {
// Normalize HTML output
.then(function() {
var pipelineOpts = _.extend({
-
// Replace links to page of summary
onRelativeLink: function(href) {
+ if (!opts.linkPages) return href;
+
var to = that.book.getPage(href);
if (to) return to.outputPath();
diff --git a/lib/utils/error.js b/lib/utils/error.js
index 5ee6a0e0d..883a7c61e 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -15,18 +15,11 @@ var ParsingError = WrappedError({
message: 'Parsing Error: {origMessage}',
type: 'parse'
});
-var GenerationError = WrappedError({
- message: 'Generation Error: {origMessage}',
+var OutputError = WrappedError({
+ message: 'Output Error: {origMessage}',
type: 'generate'
});
-// Error when output generator does not exists
-var GeneratorNotFoundError = TypedError({
- type: 'generator.not-found',
- message: 'Generator "{generator}" does not exists',
- generator: null
-});
-
// A file does not exists
var FileNotFoundError = TypedError({
type: 'file.not-found',
@@ -54,11 +47,10 @@ module.exports = {
enforce: enforce,
ParsingError: ParsingError,
- GenerationError: GenerationError,
+ OutputError: OutputError,
FileNotFoundError: FileNotFoundError,
FileOutOfScopeError: FileOutOfScopeError,
- GeneratorNotFoundError: GeneratorNotFoundError,
TemplateError: TemplateError
};
diff --git a/test/mock.js b/test/mock.js
index ce7b02775..127e35cb8 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -4,7 +4,6 @@ var tmp = require('tmp');
var path = require('path');
var Book = require('../').Book;
-var Output = require('../lib/output');
var NodeFS = require('../lib/fs/node');
require('./assertions');
@@ -58,7 +57,7 @@ function setupDefaultBook(files, opts) {
}
// Output a book with a specific generator
-function outputDefaultBook(generator, files, opts) {
+function outputDefaultBook(Output, files, opts) {
return setupDefaultBook(files, opts)
.then(function(book) {
// Parse the book
@@ -66,7 +65,7 @@ function outputDefaultBook(generator, files, opts) {
// Start generation
.then(function() {
- var output = new Output(book, generator);
+ var output = new Output(book);
return output.generate()
.thenResolve(output);
});
diff --git a/test/output-json.js b/test/output-json.js
index b2387a7a6..6eb3c48b5 100644
--- a/test/output-json.js
+++ b/test/output-json.js
@@ -1,4 +1,5 @@
var mock = require('./mock');
+var JSONOutput = require('../lib/output/json');
describe('JSON Output', function() {
@@ -6,7 +7,7 @@ describe('JSON Output', function() {
var output;
before(function() {
- return mock.outputDefaultBook('json')
+ return mock.outputDefaultBook(JSONOutput)
.then(function(_output) {
output = _output;
});
From 0d966fe19738089607de3927694ac5f2bd41f03f Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Fri, 12 Feb 2016 21:25:43 +0100
Subject: [PATCH 038/217] Separate output in FolderOutput and base
---
lib/output/base.js | 78 +++++++++++--------------------------
lib/output/folder.js | 78 +++++++++++++++++++++++++++++++++++++
lib/output/json.js | 15 ++++---
lib/output/website/index.js | 8 ----
lib/page/index.js | 20 ++--------
test/page.js | 9 +++--
6 files changed, 121 insertions(+), 87 deletions(-)
create mode 100644 lib/output/folder.js
diff --git a/lib/output/base.js b/lib/output/base.js
index 8bc27d428..39e46c211 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -1,13 +1,19 @@
var _ = require('lodash');
var Ignore = require('ignore');
-var path = require('path');
var Promise = require('../utils/promise');
-var pathUtil = require('../utils/path');
-var fs = require('../utils/fs');
var PluginsManager = require('../plugins');
+/*
+Output is like a stream interface for a parsed book
+to output "something".
+
+The process is mostly on the behavior of "onPage" and "onAsset"
+*/
+
function Output(book, type) {
+ _.bindAll(this);
+
this.book = book;
this.log = this.book.log;
@@ -26,47 +32,6 @@ function Output(book, type) {
]));
}
-// Return path to the root folder
-Output.prototype.root = function(filename) {
- return path.resolve(process.cwd(), this.book.config.get('output'));
-};
-
-// Resolve a file in the output directory
-Output.prototype.resolve = function(filename) {
- return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
-};
-
-// Write a file/buffer to the output folder
-Output.prototype.writeFile = function(filename, buf) {
- var that = this;
-
- return Promise()
- .then(function() {
- filename = that.resolve(filename);
- var folder = path.dirname(filename);
-
- // Ensure fodler exists
- return fs.mkdirp(folder);
- })
-
- // Write the file
- .then(function() {
- return fs.writeFile(filename, buf);
- });
-};
-
-// Copy a file to the output
-Output.prototype.copyFile = function(from, to) {
- var that = this;
-
- return Promise()
- .then(function() {
- to = that.resolve(to);
-
- return fs.copy(from, to);
- });
-};
-
// Start the generation, for a parsed book
Output.prototype.generate = function() {
var that = this;
@@ -83,11 +48,6 @@ Output.prototype.generate = function() {
return that.plugins.load(plugins);
})
- // Create the output folder
- .then(function() {
- return fs.mkdirp(that.root());
- })
-
// Initialize the generation
.then(function() {
return that.prepare();
@@ -104,9 +64,9 @@ Output.prototype.generate = function() {
// Process file as page or asset
if (that.book.hasPage(filename)) {
- return that.writePage(that.book.getPage(filename));
+ return that.onPage(that.book.getPage(filename));
} else {
- return that.copyAsset(filename);
+ return that.onAsset(filename);
}
});
})
@@ -122,16 +82,24 @@ Output.prototype.prepare = function() {
};
-// Copy an asset file (non-parsable), ex: images, etc
-Output.prototype.copyAsset = function(filename) {
+// Write a page (parsable file), ex: markdown, etc
+Output.prototype.onPage = function(page) {
};
-// Write a page (parsable file), ex: markdown, etc
-Output.prototype.writePage = function(page) {
+// Copy an asset file (non-parsable), ex: images, etc
+Output.prototype.onAsset = function(filename) {
};
+// Resolve an HTML link
+Output.prototype.onRelativeLink = function(currentPage, href) {
+ var to = this.book.getPage(href);
+ if (to) return to.outputPath();
+
+ return href;
+};
+
// Finish the generation
Output.prototype.finish = function() {
diff --git a/lib/output/folder.js b/lib/output/folder.js
new file mode 100644
index 000000000..96ea44390
--- /dev/null
+++ b/lib/output/folder.js
@@ -0,0 +1,78 @@
+var _ = require('lodash');
+var util = require('util');
+var path = require('path');
+
+var Output = require('./base');
+var fs = require('../utils/fs');
+var pathUtil = require('../utils/path');
+var Promise = require('../utils/promise');
+
+/*
+This output require the native fs module to output
+book as a directory (mapping assets and pages)
+*/
+
+function FolderOutput() {
+ Output.apply(this, arguments);
+}
+util.inherits(FolderOutput, Output);
+
+// Copy an asset file (non-parsable), ex: images, etc
+FolderOutput.prototype.onAsset = function(filename) {
+ return this.copyFile(
+ this.book.resolve(filename),
+ filename
+ );
+};
+
+// Prepare the generation by creating the output folder
+FolderOutput.prototype.prepare = function() {
+ return fs.mkdirp(this.root());
+};
+
+
+// ----- Utility methods -----
+
+// Return path to the root folder
+FolderOutput.prototype.root = function(filename) {
+ return path.resolve(process.cwd(), this.book.config.get('output'));
+};
+
+// Resolve a file in the output directory
+FolderOutput.prototype.resolve = function(filename) {
+ return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
+};
+
+// Copy a file to the output
+FolderOutput.prototype.copyFile = function(from, to) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ to = that.resolve(to);
+
+ return fs.copy(from, to);
+ });
+};
+
+// Write a file/buffer to the output folder
+FolderOutput.prototype.writeFile = function(filename, buf) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ filename = that.resolve(filename);
+ var folder = path.dirname(filename);
+
+ // Ensure fodler exists
+ return fs.mkdirp(folder);
+ })
+
+ // Write the file
+ .then(function() {
+ return fs.writeFile(filename, buf);
+ });
+};
+
+
+module.exports = FolderOutput;
diff --git a/lib/output/json.js b/lib/output/json.js
index b03df6e21..8cf23738f 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,18 +1,23 @@
var util = require('util');
-var Output = require('./base');
+var FolderOutput = require('./folder');
var gitbook = require('../gitbook');
function JSONOutput() {
- Output.apply(this, arguments);
+ FolderOutput.apply(this, arguments);
}
-util.inherits(JSONOutput, Output);
+util.inherits(JSONOutput, FolderOutput);
+
+// Don't copy asset on JSON output
+JSONOutput.prototype.onAsset = function(filename) {
+
+};
// Write a page (parsable file)
-JSONOutput.prototype.writePage = function(page) {
+JSONOutput.prototype.onPage = function(page) {
var that = this;
// Parse the page
- return page.parse()
+ return page.parse(this)
// Write as json
.then(function() {
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index 2b7db7341..35e81c280 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -6,14 +6,6 @@ function WebsiteOutput() {
}
util.inherits(WebsiteOutput, Output);
-// Copy an asset file
-WebsiteOutput.prototype.writeAsset = function(filename) {
- return this.copyFile(
- this.book.resolve(filename),
- filename
- );
-};
-
// Write a page (parsable file)
WebsiteOutput.prototype.writePage = function(page) {
diff --git a/lib/page/index.js b/lib/page/index.js
index 707478077..8d1bfed4b 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -80,14 +80,9 @@ Page.prototype.read = function() {
};
// Parse the page and return its content
-Page.prototype.parse = function(opts) {
+Page.prototype.parse = function(output) {
var that = this;
- opts = _.defaults(opts || {}, {
- linkPages: true
- });
-
-
this.log.debug.ln('start parsing file', this.path);
return this.read()
@@ -122,17 +117,10 @@ Page.prototype.parse = function(opts) {
// Normalize HTML output
.then(function() {
- var pipelineOpts = _.extend({
+ var pipelineOpts = {
// Replace links to page of summary
- onRelativeLink: function(href) {
- if (!opts.linkPages) return href;
-
- var to = that.book.getPage(href);
- if (to) return to.outputPath();
-
- return href;
- }
- }, opts);
+ onRelativeLink: _.partial(output.onRelativeLink, that)
+ };
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
return pipeline.output()
diff --git a/test/page.js b/test/page.js
index 90a02602a..01c6466a6 100644
--- a/test/page.js
+++ b/test/page.js
@@ -1,7 +1,8 @@
var mock = require('./mock');
+var Output = require('../lib/output/base');
describe('Page', function() {
- var book;
+ var book, output;
before(function() {
return mock.setupDefaultBook({
@@ -10,6 +11,8 @@ describe('Page', function() {
})
.then(function(_book) {
book = _book;
+ output = new Output(book);
+
return book.parse();
});
});
@@ -18,7 +21,7 @@ describe('Page', function() {
it('should add a default ID to headings', function() {
var page = book.addPage('heading.md');
- return page.parse()
+ return page.parse(output)
.then(function() {
page.content.should.be.html({
'h1#hello': {
@@ -37,7 +40,7 @@ describe('Page', function() {
before(function() {
page = book.addPage('links.md');
- return page.parse();
+ return page.parse(output);
});
it('should replace links to page to .html', function() {
From 39b6562d1445e9a6c43a377d2a978eefa6458755 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Fri, 12 Feb 2016 21:59:49 +0100
Subject: [PATCH 039/217] Add pipeline to outline svg as png
---
lib/output/base.js | 5 +++++
lib/output/folder.js | 12 +++++++++++-
lib/output/website/index.js | 8 ++++----
lib/page/html.js | 28 +++++++++++++++++++++++++++-
lib/page/index.js | 3 ++-
lib/utils/fs.js | 9 ++++++++-
lib/utils/images.js | 16 ++++++++++++++++
package.json | 2 +-
8 files changed, 74 insertions(+), 9 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 39e46c211..d301b1452 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -100,6 +100,11 @@ Output.prototype.onRelativeLink = function(currentPage, href) {
return href;
};
+// Output a SVG as a file
+Output.prototype.onOutputSVG = function(page, svg) {
+ return null;
+};
+
// Finish the generation
Output.prototype.finish = function() {
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 96ea44390..7e6ddf03f 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -5,10 +5,11 @@ var path = require('path');
var Output = require('./base');
var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
+var imagesUtil = require('../utils/images');
var Promise = require('../utils/promise');
/*
-This output require the native fs module to output
+This output requires the native fs module to output
book as a directory (mapping assets and pages)
*/
@@ -25,6 +26,15 @@ FolderOutput.prototype.onAsset = function(filename) {
);
};
+// Output a SVG as a file
+Output.prototype.onOutputSVG = function(page, svg) {
+ this.log.debug.ln('output svg from', page.path);
+ var filename = _.uniqueId('svg_') + '.png';
+
+ return imagesUtil.convertSVGBufferToPNG(svg, this.resolve(filename))
+ .thenResolve('/' + filename);
+};
+
// Prepare the generation by creating the output folder
FolderOutput.prototype.prepare = function() {
return fs.mkdirp(this.root());
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index 35e81c280..e60dae2cc 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -1,13 +1,13 @@
var util = require('util');
-var Output = require('../base');
+var FolderOutput = require('../base');
function WebsiteOutput() {
- Output.apply(this, arguments);
+ FolderOutput.apply(this, arguments);
}
-util.inherits(WebsiteOutput, Output);
+util.inherits(WebsiteOutput, FolderOutput);
// Write a page (parsable file)
-WebsiteOutput.prototype.writePage = function(page) {
+WebsiteOutput.prototype.onPage = function(page) {
};
diff --git a/lib/page/html.js b/lib/page/html.js
index b19d5ede5..948462aff 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -22,7 +22,10 @@ function HTMLPipeline(htmlString, opts) {
convertImages: true,
// Calcul new href for a relative link
- onRelativeLink: _.identity
+ onRelativeLink: _.identity,
+
+ // Output a svg, if returns null the svg is kept inlined
+ onOutputSVG: _.constant(null)
});
this.$ = cheerio.load(htmlString, {
@@ -70,6 +73,28 @@ HTMLPipeline.prototype.addHeadingIDs = function() {
});
};
+// Outline SVG from the HML
+HTMLPipeline.prototype.outlineSVG = function() {
+ var that = this;
+
+ var $svgs = this.$('svg');
+
+ return Promise.serie($svgs, function(svg) {
+ var $svg = that.$(svg);
+ var content = [
+ '',
+ renderDOM(that.$, $svg)
+ ].join('\n');
+
+ return Promise(that.opts.onOutputSVG(content))
+ .then(function(filename) {
+ if (!filename) return;
+
+ $svg.replaceWith(that.$(' ').attr('src', filename));
+ });
+ });
+};
+
// Write content to the pipeline
HTMLPipeline.prototype.output = function() {
var that = this;
@@ -77,6 +102,7 @@ HTMLPipeline.prototype.output = function() {
return Promise()
.then(this.normalizeLinks)
.then(this.addHeadingIDs)
+ .then(this.outlineSVG)
.then(function() {
return renderDOM(that.$);
});
diff --git a/lib/page/index.js b/lib/page/index.js
index 8d1bfed4b..282cffefd 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -119,7 +119,8 @@ Page.prototype.parse = function(output) {
.then(function() {
var pipelineOpts = {
// Replace links to page of summary
- onRelativeLink: _.partial(output.onRelativeLink, that)
+ onRelativeLink: _.partial(output.onRelativeLink, that),
+ onOutputSVG: _.partial(output.onOutputSVG, that)
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index 2fa6ff980..774544899 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -1,6 +1,7 @@
var fs = require('graceful-fs');
var mkdirp = require('mkdirp');
var destroy = require('destroy');
+var tmp = require('tmp');
var Promise = require('./promise');
@@ -52,6 +53,11 @@ function fileExists(filename) {
return d.promise;
}
+// Generate temporary file
+function genTmpFile(opts) {
+ return Promise.nfcall(tmp.file, opts)
+ .get(0);
+}
module.exports = {
exists: fileExists,
@@ -62,5 +68,6 @@ module.exports = {
statSync: fs.statSync,
readdir: Promise.nfbind(fs.readdir),
writeStream: writeStream,
- copy: copyFile
+ copy: copyFile,
+ tmpFile: genTmpFile
};
diff --git a/lib/utils/images.js b/lib/utils/images.js
index 3ba0f1f06..45bc0b0dc 100644
--- a/lib/utils/images.js
+++ b/lib/utils/images.js
@@ -2,6 +2,7 @@ var fs = require('fs');
var Promise = require('./promise');
var command = require('./command');
+var fs = require('./fs');
var error = require('./error');
// Convert a svg file to a pmg
@@ -21,7 +22,22 @@ function convertSVGToPNG(source, dest, options) {
});
}
+// Convert a svg buffer to a png file
+function convertSVGBufferToPNG(buf, dest) {
+ // Create a temporary SVG file to convert
+ return fs.tmpFile({
+ postfix: '.svg'
+ })
+ .then(function(tmpSvg) {
+ return fs.writeFile(tmpSvg, buf)
+ .then(function() {
+ return convertSVGToPNG(tmpSvg, dest);
+ });
+ });
+}
+
module.exports = {
convertSVGToPNG: convertSVGToPNG,
+ convertSVGBufferToPNG: convertSVGBufferToPNG,
INVALID: ['.svg']
};
\ No newline at end of file
diff --git a/package.json b/package.json
index 7dce193a5..4bd629a45 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"chokidar": "~1.0.5",
"send": "0.2.0",
"tiny-lr": "0.2.1",
- "tmp": "0.0.24",
+ "tmp": "0.0.28",
"crc": "3.2.1",
"bash-color": "0.0.3",
"urijs": "1.17.0",
From 4c6717e23488656686f276aa2b40ce1d1c7641f8 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Fri, 12 Feb 2016 23:57:43 +0100
Subject: [PATCH 040/217] Add conversion of svg to png for assets inliner
---
lib/output/assets-inliner.js | 42 ++++++++++++++++++++++++++++++++++++
lib/output/base.js | 11 +++++++---
lib/output/ebook.js | 14 ++++++++++++
lib/output/folder.js | 10 ---------
lib/page/html.js | 21 ++++++++++++++++--
lib/page/index.js | 18 +++++++++++++++-
lib/utils/command.js | 2 +-
lib/utils/fs.js | 1 +
lib/utils/images.js | 5 +----
lib/utils/location.js | 31 +++++++++++++++++++++++++-
test/all.js | 1 +
test/assets-inliner.js | 26 ++++++++++++++++++++++
test/page.js | 23 +++++++++++++++++++-
13 files changed, 182 insertions(+), 23 deletions(-)
create mode 100644 lib/output/assets-inliner.js
create mode 100644 lib/output/ebook.js
create mode 100644 test/assets-inliner.js
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
new file mode 100644
index 000000000..5f3395699
--- /dev/null
+++ b/lib/output/assets-inliner.js
@@ -0,0 +1,42 @@
+var _ = require('lodash');
+var util = require('util');
+var path = require('path');
+
+var FolderOutput = require('./folder');
+var imagesUtil = require('../utils/images');
+
+/*
+Utility mixin to inline all the assets in a book:
+ - Outline tags
+ - Convert svg images as png
+ - Download remote images
+*/
+
+function AssetsInliner() {
+ FolderOutput.apply(this, arguments);
+}
+util.inherits(AssetsInliner, FolderOutput);
+
+// Output a SVG buffer as a file
+AssetsInliner.prototype.onOutputSVG = function(page, svg) {
+ this.log.debug.ln('output svg from', page.path);
+ var filename = _.uniqueId('svg_') + '.png';
+
+ return imagesUtil.convertSVGBufferToPNG(svg, this.resolve(filename))
+ .thenResolve('/' + filename);
+};
+
+// Output an image as a file
+AssetsInliner.prototype.onOutputImage = function(page, imgFile) {
+ if (path.extname(imgFile).toLowerCase() != '.svg') {
+ return imgFile;
+ }
+
+ // Convert SVG to PNG
+ var filename = _.uniqueId('svg_') + '.png';
+ return imagesUtil.convertSVGToPNG(page.resolve(imgFile), this.resolve(filename))
+ .thenResolve('/' + filename);
+};
+
+
+module.exports = AssetsInliner;
diff --git a/lib/output/base.js b/lib/output/base.js
index d301b1452..dd62cff9c 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -84,7 +84,7 @@ Output.prototype.prepare = function() {
// Write a page (parsable file), ex: markdown, etc
Output.prototype.onPage = function(page) {
-
+ return page.parse(this);
};
// Copy an asset file (non-parsable), ex: images, etc
@@ -100,15 +100,20 @@ Output.prototype.onRelativeLink = function(currentPage, href) {
return href;
};
-// Output a SVG as a file
+// Output a SVG buffer as a file
Output.prototype.onOutputSVG = function(page, svg) {
return null;
};
+// Output an image as a file
+Output.prototype.onOutputImage = function(page, imgFile) {
+ // Don't replace it
+ return imgFile;
+};
+
// Finish the generation
Output.prototype.finish = function() {
};
-
module.exports = Output;
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
new file mode 100644
index 000000000..b968006e4
--- /dev/null
+++ b/lib/output/ebook.js
@@ -0,0 +1,14 @@
+var util = require('util');
+
+var WebsiteOutput = require('./website');
+var AssetsInliner = require('./assets-inliner');
+
+function EbookOutput() {
+ WebsiteOutput.apply(this, arguments);
+ AssetsInliner.call(this);
+}
+util.inherits(EbookOutput, AssetsInliner);
+util.inherits(EbookOutput, WebsiteOutput);
+
+
+module.exports = EbookOutput;
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 7e6ddf03f..4b1d9fa91 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -5,7 +5,6 @@ var path = require('path');
var Output = require('./base');
var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
-var imagesUtil = require('../utils/images');
var Promise = require('../utils/promise');
/*
@@ -26,15 +25,6 @@ FolderOutput.prototype.onAsset = function(filename) {
);
};
-// Output a SVG as a file
-Output.prototype.onOutputSVG = function(page, svg) {
- this.log.debug.ln('output svg from', page.path);
- var filename = _.uniqueId('svg_') + '.png';
-
- return imagesUtil.convertSVGBufferToPNG(svg, this.resolve(filename))
- .thenResolve('/' + filename);
-};
-
// Prepare the generation by creating the output folder
FolderOutput.prototype.prepare = function() {
return fs.mkdirp(this.root());
diff --git a/lib/page/html.js b/lib/page/html.js
index 948462aff..255bf48b6 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -19,11 +19,12 @@ function HTMLPipeline(htmlString, opts) {
_.bindAll(this);
this.opts = _.defaults(opts || {}, {
- convertImages: true,
-
// Calcul new href for a relative link
onRelativeLink: _.identity,
+ // Output an image
+ onImage: _.identity,
+
// Output a svg, if returns null the svg is kept inlined
onOutputSVG: _.constant(null)
});
@@ -56,7 +57,22 @@ HTMLPipeline.prototype.normalizeLinks = function() {
// External links
$a.attr('target', '_blank');
}
+ });
+};
+// Normalize images
+HTMLPipeline.prototype.normalizeImages = function() {
+ var that = this;
+
+ var $imgs = this.$('img');
+
+ return Promise.serie($imgs, function(img) {
+ var $img = that.$(img);
+
+ return Promise(that.opts.onImage($img.attr('src')))
+ .then(function(filename) {
+ $img.attr('src', filename);
+ });
});
};
@@ -101,6 +117,7 @@ HTMLPipeline.prototype.output = function() {
return Promise()
.then(this.normalizeLinks)
+ .then(this.normalizeImages)
.then(this.addHeadingIDs)
.then(this.outlineSVG)
.then(function() {
diff --git a/lib/page/index.js b/lib/page/index.js
index 282cffefd..e96f89bdf 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -4,6 +4,7 @@ var parsers = require('gitbook-parsers');
var error = require('../utils/error');
var pathUtil = require('../utils/path');
+var location = require('../utils/location');
var HTMLPipeline = require('./html');
/*
@@ -62,6 +63,21 @@ Page.prototype.outputPath = function(ext) {
return output;
};
+// Resolve a filename relative to this page
+// It returns a path relative to the book root folder
+Page.prototype.resolveLocal = function() {
+ var dir = path.dirname(this.path);
+ var file = path.join.apply(path, _.toArray(arguments));
+
+ return location.toAbsolute(file, dir, '');
+};
+
+// Resolve a filename relative to this page
+// It returns an absolute path for the FS
+Page.prototype.resolve = function() {
+ return this.book.resolve(this.resolveLocal.apply(this, arguments));
+};
+
// Update content of the page
Page.prototype.update = function(content) {
this.content = content;
@@ -118,8 +134,8 @@ Page.prototype.parse = function(output) {
// Normalize HTML output
.then(function() {
var pipelineOpts = {
- // Replace links to page of summary
onRelativeLink: _.partial(output.onRelativeLink, that),
+ onImage: _.partial(output.onOutputImage, that),
onOutputSVG: _.partial(output.onOutputSVG, that)
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
diff --git a/lib/utils/command.js b/lib/utils/command.js
index 4269d6cfb..c3e33c7f0 100644
--- a/lib/utils/command.js
+++ b/lib/utils/command.js
@@ -19,7 +19,7 @@ function spawn(command, args, options) {
return Promise.reject(new Error('Command execution is not possible on this platform'));
}
- var d = Promise.deferred();
+ var d = Promise.defer();
var child = childProcess.spawn(command, args, options);
child.on('error', function(error) {
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index 774544899..ea9546beb 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -61,6 +61,7 @@ function genTmpFile(opts) {
module.exports = {
exists: fileExists,
+ existsSync: fs.existsSync,
mkdirp: Promise.nfbind(mkdirp),
readFile: Promise.nfbind(fs.readFile),
writeFile: Promise.nfbind(fs.writeFile),
diff --git a/lib/utils/images.js b/lib/utils/images.js
index 45bc0b0dc..8169b066a 100644
--- a/lib/utils/images.js
+++ b/lib/utils/images.js
@@ -1,5 +1,3 @@
-var fs = require('fs');
-
var Promise = require('./promise');
var command = require('./command');
var fs = require('./fs');
@@ -38,6 +36,5 @@ function convertSVGBufferToPNG(buf, dest) {
module.exports = {
convertSVGToPNG: convertSVGToPNG,
- convertSVGBufferToPNG: convertSVGBufferToPNG,
- INVALID: ['.svg']
+ convertSVGBufferToPNG: convertSVGBufferToPNG
};
\ No newline at end of file
diff --git a/lib/utils/location.js b/lib/utils/location.js
index efe1425a1..3f3cb3722 100644
--- a/lib/utils/location.js
+++ b/lib/utils/location.js
@@ -1,4 +1,5 @@
var url = require('url');
+var path = require('path');
// Is the url an external url
function isExternal(href) {
@@ -24,8 +25,36 @@ function isAnchor(href) {
}
}
+// Normalize a path to be a link
+function normalize(s) {
+ return s.replace(/\\/g, '/');
+}
+
+// Convert relative to absolute path
+// dir: directory parent of the file currently in rendering process
+// outdir: directory parent from the html output
+function toAbsolute(_href, dir, outdir) {
+ outdir = outdir == undefined? dir : outdir;
+
+ if (isExternal(_href)) return _href;
+
+ // Path "_href" inside the base folder
+ var hrefInRoot = path.normalize(path.join(dir, _href));
+ if (_href[0] == '/') hrefInRoot = path.normalize(_href.slice(1));
+
+ // Make it relative to output
+ _href = path.relative(outdir, hrefInRoot);
+
+ // Normalize windows paths
+ _href = normalize(_href);
+
+ return _href;
+}
+
module.exports = {
isExternal: isExternal,
isRelative: isRelative,
- isAnchor: isAnchor
+ isAnchor: isAnchor,
+ normalize: normalize,
+ toAbsolute: toAbsolute
};
diff --git a/test/all.js b/test/all.js
index 08be793ac..242324bd4 100644
--- a/test/all.js
+++ b/test/all.js
@@ -13,3 +13,4 @@ require('./template');
// Output
require('./output-json');
+require('./assets-inliner');
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
new file mode 100644
index 000000000..479d7884b
--- /dev/null
+++ b/test/assets-inliner.js
@@ -0,0 +1,26 @@
+var mock = require('./mock');
+var AssetsInliner = require('../lib/output/assets-inliner');
+
+describe('Assets Inliner Output', function() {
+
+ describe('SVG', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook(AssetsInliner, {
+ 'README.md': '',
+ 'test.svg': ' '
+ })
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ it('should correctly convert to PNG', function() {
+ var readme = output.book.getPage('README.md');
+ console.log(readme.content)
+ });
+
+ });
+});
+
diff --git a/test/page.js b/test/page.js
index 01c6466a6..9007c4522 100644
--- a/test/page.js
+++ b/test/page.js
@@ -7,7 +7,8 @@ describe('Page', function() {
before(function() {
return mock.setupDefaultBook({
'heading.md': '# Hello\n\n## World',
- 'links.md': '[link](hello.md) [readme](README.md)'
+ 'links.md': '[link](hello.md) [readme](README.md)',
+ 'folder/paths.md': ''
})
.then(function(_book) {
book = _book;
@@ -17,6 +18,26 @@ describe('Page', function() {
});
});
+ describe('.resolveLocal', function() {
+ it('should correctly resolve path to file', function() {
+ var page = book.addPage('heading.md');
+
+ page.resolveLocal('test.png').should.equal('test.png');
+ page.resolveLocal('/test.png').should.equal('test.png');
+ page.resolveLocal('test/hello.png').should.equal('test/hello.png');
+ page.resolveLocal('/test/hello.png').should.equal('test/hello.png');
+ });
+
+ it('should correctly resolve path to file (2)', function() {
+ var page = book.addPage('folder/paths.md');
+
+ page.resolveLocal('test.png').should.equal('folder/test.png');
+ page.resolveLocal('/test.png').should.equal('test.png');
+ page.resolveLocal('test/hello.png').should.equal('folder/test/hello.png');
+ page.resolveLocal('/test/hello.png').should.equal('test/hello.png');
+ });
+ });
+
describe('Headings', function() {
it('should add a default ID to headings', function() {
var page = book.addPage('heading.md');
From 272f30af532b247e2b13d2d0b59c89a041723e9a Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Sat, 13 Feb 2016 10:43:22 +0100
Subject: [PATCH 041/217] Complete test for convertion of svgs
---
lib/output/assets-inliner.js | 13 +++++++++++--
lib/page/index.js | 8 ++++++++
lib/utils/location.js | 8 +++++++-
test/assets-inliner.js | 14 ++++++++++++--
test/page.js | 12 ++++++++++++
5 files changed, 50 insertions(+), 5 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index 5f3395699..65ecbfa23 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -22,8 +22,13 @@ AssetsInliner.prototype.onOutputSVG = function(page, svg) {
this.log.debug.ln('output svg from', page.path);
var filename = _.uniqueId('svg_') + '.png';
+ // Convert svg buffer to a png file
return imagesUtil.convertSVGBufferToPNG(svg, this.resolve(filename))
- .thenResolve('/' + filename);
+
+ // Return relative path from the page
+ .thenResolve(function() {
+ return page.relative('/' + filename);
+ });
};
// Output an image as a file
@@ -35,7 +40,11 @@ AssetsInliner.prototype.onOutputImage = function(page, imgFile) {
// Convert SVG to PNG
var filename = _.uniqueId('svg_') + '.png';
return imagesUtil.convertSVGToPNG(page.resolve(imgFile), this.resolve(filename))
- .thenResolve('/' + filename);
+
+ // Return relative path from the page
+ .thenResolve(function() {
+ return page.relative('/' + filename);
+ });
};
diff --git a/lib/page/index.js b/lib/page/index.js
index e96f89bdf..77b995065 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -78,6 +78,14 @@ Page.prototype.resolve = function() {
return this.book.resolve(this.resolveLocal.apply(this, arguments));
};
+// Convert an absolite path to a relative path from this page
+Page.prototype.relative = function(name) {
+ return location.relative(
+ this.resolve('.'),
+ this.resolve(name)
+ );
+};
+
// Update content of the page
Page.prototype.update = function(content) {
this.content = content;
diff --git a/lib/utils/location.js b/lib/utils/location.js
index 3f3cb3722..09fa93a60 100644
--- a/lib/utils/location.js
+++ b/lib/utils/location.js
@@ -51,10 +51,16 @@ function toAbsolute(_href, dir, outdir) {
return _href;
}
+// Convert an absolute path to a relative patg
+function relative(dir, file) {
+ return normalize(path.relative(dir, file));
+}
+
module.exports = {
isExternal: isExternal,
isRelative: isRelative,
isAnchor: isAnchor,
normalize: normalize,
- toAbsolute: toAbsolute
+ toAbsolute: toAbsolute,
+ relative: relative
};
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index 479d7884b..0636a1b5d 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -1,3 +1,5 @@
+var cheerio = require('cheerio');
+
var mock = require('./mock');
var AssetsInliner = require('../lib/output/assets-inliner');
@@ -9,7 +11,7 @@ describe('Assets Inliner Output', function() {
before(function() {
return mock.outputDefaultBook(AssetsInliner, {
'README.md': '',
- 'test.svg': ' '
+ 'test.svg': ' '
})
.then(function(_output) {
output = _output;
@@ -18,7 +20,15 @@ describe('Assets Inliner Output', function() {
it('should correctly convert to PNG', function() {
var readme = output.book.getPage('README.md');
- console.log(readme.content)
+ var $ = cheerio.load(readme.content);
+
+ // Is there an image?
+ var $img = $('img');
+ $img.length.should.equal(1);
+
+ // Does the file exists
+ var src = $img.attr('src');
+ output.should.have.file(src);
});
});
diff --git a/test/page.js b/test/page.js
index 9007c4522..d844d1331 100644
--- a/test/page.js
+++ b/test/page.js
@@ -38,6 +38,18 @@ describe('Page', function() {
});
});
+ describe('.relative', function() {
+ it('should correctly resolve absolute path in the book', function() {
+ var page = book.addPage('heading.md');
+ var page2 = book.addPage('folder/paths.md');
+
+ page.relative('/test.png').should.equal('test.png');
+ page.relative('test.png').should.equal('test.png');
+ page2.relative('/test.png').should.equal('../test.png');
+ page2.relative('test.png').should.equal('test.png');
+ });
+ });
+
describe('Headings', function() {
it('should add a default ID to headings', function() {
var page = book.addPage('heading.md');
From 3154d37e41da68a16ef29ec69d8896acd9da336a Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Sat, 13 Feb 2016 10:49:56 +0100
Subject: [PATCH 042/217] Add more test for svg
---
test/assets-inliner.js | 23 +++++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index 0636a1b5d..dd63b4152 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -9,18 +9,21 @@ describe('Assets Inliner Output', function() {
var output;
before(function() {
+ var SVG = ' ';
+
return mock.outputDefaultBook(AssetsInliner, {
'README.md': '',
- 'test.svg': ' '
+ 'inline.md': 'This is a svg: '+SVG,
+ 'test.svg': '' + SVG
})
.then(function(_output) {
output = _output;
});
});
- it('should correctly convert to PNG', function() {
- var readme = output.book.getPage('README.md');
- var $ = cheerio.load(readme.content);
+ it('should correctly SVG files convert to PNG', function() {
+ var page = output.book.getPage('README.md');
+ var $ = cheerio.load(page.content);
// Is there an image?
var $img = $('img');
@@ -31,6 +34,18 @@ describe('Assets Inliner Output', function() {
output.should.have.file(src);
});
+ it('should correctly inline SVG convert to PNG', function() {
+ var page = output.book.addPage('README.md');
+ var $ = cheerio.load(page.content);
+
+ // Is there an image?
+ var $img = $('img');
+ $img.length.should.equal(1);
+
+ // Does the file exists
+ var src = $img.attr('src');
+ output.should.have.file(src);
+ });
});
});
From d8ed34beba05bde85af420cc4e42a065021653dc Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Sat, 13 Feb 2016 13:56:04 +0100
Subject: [PATCH 043/217] Add method to fetch remote image with AssetsInliner
---
lib/output/assets-inliner.js | 51 +++++++++++++++++++++++++++++-------
lib/output/base.js | 4 +--
lib/output/folder.js | 6 +++++
lib/utils/fs.js | 28 +++++++++++++++++++-
package.json | 2 +-
test/assets-inliner.js | 4 +++
6 files changed, 81 insertions(+), 14 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index 65ecbfa23..2156bef41 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -3,7 +3,12 @@ var util = require('util');
var path = require('path');
var FolderOutput = require('./folder');
+var Promise = require('../utils/promise');
+var fs = require('../utils/fs');
var imagesUtil = require('../utils/images');
+var location = require('../utils/location');
+
+var DEFAULT_ASSETS_FOLDER = 'assets';
/*
Utility mixin to inline all the assets in a book:
@@ -32,19 +37,45 @@ AssetsInliner.prototype.onOutputSVG = function(page, svg) {
};
// Output an image as a file
-AssetsInliner.prototype.onOutputImage = function(page, imgFile) {
- if (path.extname(imgFile).toLowerCase() != '.svg') {
- return imgFile;
+AssetsInliner.prototype.onOutputImage = function(page, src) {
+ var that = this;
+ var isSVG = false;
+ var ext = path.extname(src).toLowerCase();
+ if (ext == '.svg') {
+ isSVG = false;
+ ext = '.png';
}
- // Convert SVG to PNG
- var filename = _.uniqueId('svg_') + '.png';
- return imagesUtil.convertSVGToPNG(page.resolve(imgFile), this.resolve(filename))
+ return Promise()
- // Return relative path from the page
- .thenResolve(function() {
- return page.relative('/' + filename);
- });
+ // Allocate a new file
+ .then(function() {
+
+ return that.
+ })
+
+ // Download file if external
+ .then(function() {
+ if (!location.isExternal(src)) return;
+
+ return fs.download(src, )
+
+ })
+ .then(function() {
+ if (path.extname(src).toLowerCase() != '.svg') {
+ return src;
+ }
+
+ // Convert SVG to PNG
+ var filename = _.uniqueId('svg_') + '.png';
+ return imagesUtil.convertSVGToPNG(page.resolve(src), this.resolve(filename))
+ .thenResolve('/' + filename);
+ })
+
+ // Return relative path from the page
+ .thenResolve(function(filename) {
+ return page.relative('/' + filename);
+ });
};
diff --git a/lib/output/base.js b/lib/output/base.js
index dd62cff9c..688ecb7f3 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -106,9 +106,9 @@ Output.prototype.onOutputSVG = function(page, svg) {
};
// Output an image as a file
+// Normalize the relative link
Output.prototype.onOutputImage = function(page, imgFile) {
- // Don't replace it
- return imgFile;
+ return page.relative(imgFile);
};
// Finish the generation
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 4b1d9fa91..aabdcc483 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -74,5 +74,11 @@ FolderOutput.prototype.writeFile = function(filename, buf) {
});
};
+// Create a new unique file
+// Returns its filename
+FolderOutput.prototype.createNewFile = function(base, filename) {
+
+};
+
module.exports = FolderOutput;
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index ea9546beb..bf5e51d46 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -2,6 +2,8 @@ var fs = require('graceful-fs');
var mkdirp = require('mkdirp');
var destroy = require('destroy');
var tmp = require('tmp');
+var request = require('request');
+var path = require('path');
var Promise = require('./promise');
@@ -59,6 +61,28 @@ function genTmpFile(opts) {
.get(0);
}
+// Download an image
+function download(uri, dest) {
+ return writeStream(dest, request(uri));
+}
+
+// Find a filename available in a folder
+function uniqueFilename(base, filename) {
+ var ext = path.extname(filename);
+ filename = path.resolve(base, filename);
+ filename = path.join(path.dirname(filename), path.basename(filename, ext));
+
+ var _filename = filename+ext;
+
+ var i = 0;
+ while (fs.existsSync(filename)) {
+ _filename = filename + '_' + i + ext;
+ i = i + 1;
+ }
+
+ return path.relative(base, _filename);
+}
+
module.exports = {
exists: fileExists,
existsSync: fs.existsSync,
@@ -70,5 +94,7 @@ module.exports = {
readdir: Promise.nfbind(fs.readdir),
writeStream: writeStream,
copy: copyFile,
- tmpFile: genTmpFile
+ tmpFile: genTmpFile,
+ download: download,
+ uniqueFilename: uniqueFilename
};
diff --git a/package.json b/package.json
index 4bd629a45..1d0b52053 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"crc": "3.2.1",
"bash-color": "0.0.3",
"urijs": "1.17.0",
- "request": "2.51.0",
+ "request": "2.69.0",
"npm": "2.4.1",
"dom-serializer": "0.1.0",
"spawn-cmd": "0.0.2",
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index dd63b4152..cb02e429b 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -47,5 +47,9 @@ describe('Assets Inliner Output', function() {
output.should.have.file(src);
});
});
+
+ describe('Remote Assets', function() {
+
+ });
});
From d2aad34935cb243dcdaeabfc28dce150c68f6340 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 13 Feb 2016 15:04:18 +0100
Subject: [PATCH 044/217] Improve performance of svg conversion
---
lib/output/assets-inliner.js | 95 +++++++++++++++++++++++++++---------
lib/output/folder.js | 18 ++++++-
lib/utils/fs.js | 2 +-
test/assets-inliner.js | 5 +-
4 files changed, 94 insertions(+), 26 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index 2156bef41..fb348b426 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -1,6 +1,6 @@
-var _ = require('lodash');
var util = require('util');
var path = require('path');
+var crc = require('crc');
var FolderOutput = require('./folder');
var Promise = require('../utils/promise');
@@ -13,52 +13,50 @@ var DEFAULT_ASSETS_FOLDER = 'assets';
/*
Utility mixin to inline all the assets in a book:
- Outline tags
- - Convert svg images as png
- Download remote images
+ - Convert .svg images as png
*/
function AssetsInliner() {
FolderOutput.apply(this, arguments);
+
+ // Map of svg already converted
+ this.svgs = {};
+ this.inlineSvgs = {};
+
+ // Map of images already downloaded
+ this.downloaded = {};
}
util.inherits(AssetsInliner, FolderOutput);
// Output a SVG buffer as a file
AssetsInliner.prototype.onOutputSVG = function(page, svg) {
this.log.debug.ln('output svg from', page.path);
- var filename = _.uniqueId('svg_') + '.png';
// Convert svg buffer to a png file
- return imagesUtil.convertSVGBufferToPNG(svg, this.resolve(filename))
+ return this.convertSVGBuffer(svg)
// Return relative path from the page
- .thenResolve(function() {
+ .then(function(filename) {
return page.relative('/' + filename);
});
};
+
// Output an image as a file
AssetsInliner.prototype.onOutputImage = function(page, src) {
var that = this;
- var isSVG = false;
- var ext = path.extname(src).toLowerCase();
- if (ext == '.svg') {
- isSVG = false;
- ext = '.png';
- }
return Promise()
- // Allocate a new file
- .then(function() {
-
- return that.
- })
-
// Download file if external
.then(function() {
if (!location.isExternal(src)) return;
- return fs.download(src, )
+ return that.downloadAsset(src)
+ .then(function(_asset) {
+ src = '/' + _asset;
+ });
})
.then(function() {
@@ -67,16 +65,69 @@ AssetsInliner.prototype.onOutputImage = function(page, src) {
}
// Convert SVG to PNG
- var filename = _.uniqueId('svg_') + '.png';
- return imagesUtil.convertSVGToPNG(page.resolve(src), this.resolve(filename))
- .thenResolve('/' + filename);
+ return that.convertSVGFile(page.resolve(src));
})
// Return relative path from the page
- .thenResolve(function(filename) {
+ .then(function(filename) {
return page.relative('/' + filename);
});
};
+// Download an asset if not already download; returns the output file
+AssetsInliner.prototype.downloadAsset = function(src) {
+ if (this.downloaded[src]) return Promise(this.downloaded[src]);
+
+ var that = this;
+ var ext = path.extname(src);
+ var hash = crc.crc32(src).toString(16);
+
+ // Create new file
+ return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + ext)
+ .then(function(filename) {
+ that.downloaded[src] = filename;
+
+ that.log.debug.ln('downloading asset', src);
+ return fs.download(src, filename)
+ .thenResolve(filename);
+ });
+};
+
+// Convert a .svg into an .png
+// Return the output filename for the .png
+AssetsInliner.prototype.convertSVGFile = function(src) {
+ if (this.svgs[src]) return Promise(this.svgs[src]);
+
+ var that = this;
+ var hash = crc.crc32(src).toString(16);
+
+ // Create new file
+ return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
+ .then(function(filename) {
+ that.svgs[src] = filename;
+
+ return imagesUtil.convertSVGToPNG(src, that.resolve(filename))
+ .thenResolve(filename);
+ });
+};
+
+// Convert an inline svg into an .png
+// Return the output filename for the .png
+AssetsInliner.prototype.convertSVGBuffer = function(buf) {
+ var that = this;
+ var hash = crc.crc32(buf).toString(16);
+
+ // Already converted?
+ if (this.inlineSvgs[hash]) return Promise(this.inlineSvgs[hash]);
+
+ return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
+ .then(function(filename) {
+ that.inlineSvgs[hash] = filename;
+
+ return imagesUtil.convertSVGBufferToPNG(buf, that.resolve(filename))
+ .thenResolve(filename);
+ });
+};
+
module.exports = AssetsInliner;
diff --git a/lib/output/folder.js b/lib/output/folder.js
index aabdcc483..8b899f612 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -34,7 +34,7 @@ FolderOutput.prototype.prepare = function() {
// ----- Utility methods -----
// Return path to the root folder
-FolderOutput.prototype.root = function(filename) {
+FolderOutput.prototype.root = function() {
return path.resolve(process.cwd(), this.book.config.get('output'));
};
@@ -74,10 +74,26 @@ FolderOutput.prototype.writeFile = function(filename, buf) {
});
};
+// Return true if a file exists in the output folder
+FolderOutput.prototype.hasFile = function(filename) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ return fs.exists(that.resolve(filename));
+ });
+};
+
+
// Create a new unique file
// Returns its filename
FolderOutput.prototype.createNewFile = function(base, filename) {
+ if (!filename) {
+ filename = path.basename(filename);
+ base = path.dirname(base);
+ }
+ return fs.uniqueFilename(this.resolve(base), filename);
};
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index bf5e51d46..3f1c11f13 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -80,7 +80,7 @@ function uniqueFilename(base, filename) {
i = i + 1;
}
- return path.relative(base, _filename);
+ return Promise(path.relative(base, _filename));
}
module.exports = {
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index cb02e429b..76b04f6ae 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -14,7 +14,8 @@ describe('Assets Inliner Output', function() {
return mock.outputDefaultBook(AssetsInliner, {
'README.md': '',
'inline.md': 'This is a svg: '+SVG,
- 'test.svg': '' + SVG
+ 'test.svg': '' + SVG,
+ 'SUMMARY.md': '* [inline](inline.md)\n\n'
})
.then(function(_output) {
output = _output;
@@ -35,7 +36,7 @@ describe('Assets Inliner Output', function() {
});
it('should correctly inline SVG convert to PNG', function() {
- var page = output.book.addPage('README.md');
+ var page = output.book.getPage('inline.md');
var $ = cheerio.load(page.content);
// Is there an image?
From ee3faaad1106bb773078e580c9c1611c8f31607c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 13 Feb 2016 15:28:07 +0100
Subject: [PATCH 045/217] Complete assets inliner
---
cf33a0ef.svg | 259 +++++++++++++++++++++++++++++++++++
lib/output/assets-inliner.js | 4 +-
lib/output/base.js | 28 ++--
lib/output/folder.js | 17 ++-
lib/utils/fs.js | 9 +-
test/assets-inliner.js | 90 +++++++-----
6 files changed, 356 insertions(+), 51 deletions(-)
create mode 100644 cf33a0ef.svg
diff --git a/cf33a0ef.svg b/cf33a0ef.svg
new file mode 100644
index 000000000..e4b7e589a
--- /dev/null
+++ b/cf33a0ef.svg
@@ -0,0 +1,259 @@
+
+
+ SVG Logo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SVG
+
+
+
+
+
+
+
+
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index fb348b426..97335050d 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -65,7 +65,7 @@ AssetsInliner.prototype.onOutputImage = function(page, src) {
}
// Convert SVG to PNG
- return that.convertSVGFile(page.resolve(src));
+ return that.convertSVGFile(that.resolveForPage(page, src));
})
// Return relative path from the page
@@ -88,7 +88,7 @@ AssetsInliner.prototype.downloadAsset = function(src) {
that.downloaded[src] = filename;
that.log.debug.ln('downloading asset', src);
- return fs.download(src, filename)
+ return fs.download(src, that.resolve(filename))
.thenResolve(filename);
});
};
diff --git a/lib/output/base.js b/lib/output/base.js
index 688ecb7f3..9acfb87b8 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -57,17 +57,27 @@ Output.prototype.generate = function() {
.then(function() {
return that.book.fs.listAllFiles(that.book.root);
})
- .then(function(files) {
- return Promise.serie(files, function(filename) {
- // Ignore file present in a language book
- if (isMultilingual && that.book.isInLanguageBook(filename)) return;
- // Process file as page or asset
- if (that.book.hasPage(filename)) {
+ // We want to process assets first, then pages
+ // Since pages can have logic based on existance of assets
+ .then(function(files) {
+ // Ignore file present in a language book
+ files = _.filter(files, function(filename) {
+ return !(isMultilingual && that.book.isInLanguageBook(filename));
+ });
+
+ // List assets
+ var byTypes = _.groupBy(files, function(filename) {
+ return (that.book.hasPage(filename)? 'page' : 'asset');
+ });
+
+ return Promise.serie(byTypes.asset, function(filename) {
+ return that.onAsset(filename);
+ })
+ .then(function() {
+ return Promise.serie(byTypes.page, function(filename) {
return that.onPage(that.book.getPage(filename));
- } else {
- return that.onAsset(filename);
- }
+ });
});
})
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 8b899f612..90a33511b 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -43,6 +43,13 @@ FolderOutput.prototype.resolve = function(filename) {
return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
};
+// Resolve a file path from a page (in the output folder)
+FolderOutput.prototype.resolveForPage = function(page, filename) {
+ var abs = page.resolveLocal(filename);
+ return this.resolve(abs);
+};
+
+
// Copy a file to the output
FolderOutput.prototype.copyFile = function(from, to) {
var that = this;
@@ -88,12 +95,20 @@ FolderOutput.prototype.hasFile = function(filename) {
// Create a new unique file
// Returns its filename
FolderOutput.prototype.createNewFile = function(base, filename) {
+ var that = this;
+
if (!filename) {
filename = path.basename(filename);
base = path.dirname(base);
}
- return fs.uniqueFilename(this.resolve(base), filename);
+ return fs.uniqueFilename(this.resolve(base), filename)
+ .then(function(out) {
+ out = path.join(base, out);
+
+ return fs.ensure(that.resolve(out))
+ .thenResolve(out);
+ });
};
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index 3f1c11f13..c0ff0c99a 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -83,6 +83,12 @@ function uniqueFilename(base, filename) {
return Promise(path.relative(base, _filename));
}
+// Create all required folder to create a file
+function ensureFile(filename) {
+ var base = path.dirname(filename);
+ return Promise.nfcall(mkdirp, base);
+}
+
module.exports = {
exists: fileExists,
existsSync: fs.existsSync,
@@ -96,5 +102,6 @@ module.exports = {
copy: copyFile,
tmpFile: genTmpFile,
download: download,
- uniqueFilename: uniqueFilename
+ uniqueFilename: uniqueFilename,
+ ensure: ensureFile
};
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index 76b04f6ae..1f00b5dea 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -1,56 +1,70 @@
var cheerio = require('cheerio');
+var path = require('path');
var mock = require('./mock');
var AssetsInliner = require('../lib/output/assets-inliner');
describe('Assets Inliner Output', function() {
+ var output;
+
+ before(function() {
+ var SVG = ' ';
+
+ return mock.outputDefaultBook(AssetsInliner, {
+ 'README.md': '',
+
+ // test for SVGS
+ 'svg_file.md': '',
+ 'svg_inline.md': 'This is a svg: '+SVG,
+ 'test.svg': '' + SVG,
+
+ // test for remote files
+ 'remote_png.md': '',
+ 'remote_svg.md': '',
+
+ 'SUMMARY.md': '* [svg inline](svg_inline.md)\n' +
+ '* [svg file](svg_file.md)\n' +
+ '* [remote png file](remote_png.md)\n' +
+ '* [remote svg file](remote_svg.md)\n' +
+ '\n\n'
+ })
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ function testImageInPage(filename) {
+ var page = output.book.getPage(filename);
+ var $ = cheerio.load(page.content);
+
+ // Is there an image?
+ var $img = $('img');
+ $img.length.should.equal(1);
+
+ // Does the file exists
+ var src = $img.attr('src');
+ output.should.have.file(src);
+ path.extname(src).should.equal('.png');
+ }
describe('SVG', function() {
- var output;
-
- before(function() {
- var SVG = ' ';
-
- return mock.outputDefaultBook(AssetsInliner, {
- 'README.md': '',
- 'inline.md': 'This is a svg: '+SVG,
- 'test.svg': '' + SVG,
- 'SUMMARY.md': '* [inline](inline.md)\n\n'
- })
- .then(function(_output) {
- output = _output;
- });
+ it('should correctly convert SVG files to PNG', function() {
+ testImageInPage('svg_file.md');
});
- it('should correctly SVG files convert to PNG', function() {
- var page = output.book.getPage('README.md');
- var $ = cheerio.load(page.content);
-
- // Is there an image?
- var $img = $('img');
- $img.length.should.equal(1);
-
- // Does the file exists
- var src = $img.attr('src');
- output.should.have.file(src);
- });
-
- it('should correctly inline SVG convert to PNG', function() {
- var page = output.book.getPage('inline.md');
- var $ = cheerio.load(page.content);
-
- // Is there an image?
- var $img = $('img');
- $img.length.should.equal(1);
-
- // Does the file exists
- var src = $img.attr('src');
- output.should.have.file(src);
+ it('should correctly convert inline SVG to PNG', function() {
+ testImageInPage('svg_inline.md');
});
});
describe('Remote Assets', function() {
+ it('should correctly download a PNG file', function() {
+ testImageInPage('remote_png.md');
+ });
+ it('should correctly download then convert a remote SVG to PNG', function() {
+ testImageInPage('remote_svg.md');
+ });
});
});
From 71059e3ea3ea9af156987a0dd4cb0a5457b0316c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 13 Feb 2016 15:51:14 +0100
Subject: [PATCH 046/217] Ignore Summary and Langs in output
---
lib/output/base.js | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 9acfb87b8..853fa30a1 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -28,7 +28,11 @@ function Output(book, type) {
'.bookignore',
// The configuration file should not be copied in the output
- this.book.config.path
+ this.book.config.path,
+
+ // Structure file to ignore
+ this.book.summary.path,
+ this.book.langs.path
]));
}
@@ -61,15 +65,20 @@ Output.prototype.generate = function() {
// We want to process assets first, then pages
// Since pages can have logic based on existance of assets
.then(function(files) {
- // Ignore file present in a language book
- files = _.filter(files, function(filename) {
- return !(isMultilingual && that.book.isInLanguageBook(filename));
- });
+ // Split into pages/assets
+ var byTypes = _.chain(files)
+ .filter(that.ignore.createFilter())
- // List assets
- var byTypes = _.groupBy(files, function(filename) {
- return (that.book.hasPage(filename)? 'page' : 'asset');
- });
+ // Ignore file present in a language book
+ .filter(function(filename) {
+ return !(isMultilingual && that.book.isInLanguageBook(filename));
+ })
+
+ .groupBy(function(filename) {
+ return (that.book.hasPage(filename)? 'page' : 'asset');
+ })
+
+ .value();
return Promise.serie(byTypes.asset, function(filename) {
return that.onAsset(filename);
From a65f888a77c48968087d23750e5fa4d55f2a8686 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 11:10:08 +0100
Subject: [PATCH 047/217] Start moving template and conrefs on Output
---
lib/output/base.js | 13 +++++++++++--
lib/output/conrefs.js | 22 ++++++++++++++++++++++
lib/output/website/index.js | 2 ++
lib/template/index.js | 7 ++++---
lib/utils/fs.js | 7 +++++++
lib/utils/git.js | 17 +++++++++++++++--
test/all.js | 1 +
test/website-theme.js | 17 +++++++++++++++++
8 files changed, 79 insertions(+), 7 deletions(-)
create mode 100644 lib/output/conrefs.js
create mode 100644 test/website-theme.js
diff --git a/lib/output/base.js b/lib/output/base.js
index 853fa30a1..8142761ae 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -3,6 +3,7 @@ var Ignore = require('ignore');
var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
+var TemplateEngine = require('../template');
/*
Output is like a stream interface for a parsed book
@@ -11,15 +12,18 @@ to output "something".
The process is mostly on the behavior of "onPage" and "onAsset"
*/
-function Output(book, type) {
+function Output(book) {
_.bindAll(this);
this.book = book;
this.log = this.book.log;
- this.type = type;
+ // Create plugins manager
this.plugins = new PluginsManager(book);
+ // Create template engine
+ this.template = new TemplateEngine(this);
+
// Files to ignore in output
this.ignore = Ignore();
this.ignore.addPattern(_.compact([
@@ -130,6 +134,11 @@ Output.prototype.onOutputImage = function(page, imgFile) {
return page.relative(imgFile);
};
+// By default don;t resolve conrefs
+Output.prototype.onResolveTemplate = function(from, to) {
+
+};
+
// Finish the generation
Output.prototype.finish = function() {
diff --git a/lib/output/conrefs.js b/lib/output/conrefs.js
new file mode 100644
index 000000000..2883c7b42
--- /dev/null
+++ b/lib/output/conrefs.js
@@ -0,0 +1,22 @@
+var util = require('util');
+
+var Output = require('./base');
+var Git = require('../utils/git');
+var pathUtil = require('../utils/path');
+
+/*
+Middleware for output to resolve git conrefs
+*/
+
+function ConrefsLoader() {
+ Output.apply(this, arguments);
+ this.git = new Git();
+}
+util.inherits(ConrefsLoader, Output);
+
+// Resolve an include in the template engine
+ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
+
+};
+
+module.exports = ConrefsLoader;
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index e60dae2cc..a35cafe42 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -1,6 +1,8 @@
var util = require('util');
var FolderOutput = require('../base');
+var Theme = require('./theme');
+
function WebsiteOutput() {
FolderOutput.apply(this, arguments);
}
diff --git a/lib/template/index.js b/lib/template/index.js
index b59dd0482..3f74267e3 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -21,9 +21,10 @@ function normBlockResult(blk) {
return blk;
}
-function TemplateEngine(book) {
- this.book = book;
- this.log = book.log;
+function TemplateEngine(output) {
+ this.output = output;
+ this.book = output.book;
+ this.log = this.book.log;
// Create file loader
this.loader = new Loader(this);
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index c0ff0c99a..840d1f9cb 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -61,6 +61,12 @@ function genTmpFile(opts) {
.get(0);
}
+// Generate temporary dir
+function genTmpDir(opts) {
+ return Promise.nfcall(tmp.dir, opts)
+ .get(0);
+}
+
// Download an image
function download(uri, dest) {
return writeStream(dest, request(uri));
@@ -101,6 +107,7 @@ module.exports = {
writeStream: writeStream,
copy: copyFile,
tmpFile: genTmpFile,
+ tmpDir: genTmpDir,
download: download,
uniqueFilename: uniqueFilename,
ensure: ensureFile
diff --git a/lib/utils/git.js b/lib/utils/git.js
index baf0fab1f..fc75ee375 100644
--- a/lib/utils/git.js
+++ b/lib/utils/git.js
@@ -6,11 +6,12 @@ var URI = require('urijs');
var pathUtil = require('./path');
var Promise = require('./promise');
var command = require('./command');
+var fs = require('./fs');
var GIT_PREFIX = 'git+';
function Git(tmpDir) {
- this.tmpDir = tmpDir;
+ this.tmpDir;
this.cloned = {};
}
@@ -19,11 +20,23 @@ Git.prototype.repoID = function(host, ref) {
return crc.crc32(host+'#'+(ref || '')).toString(16);
};
+// Allocate a temporary folder for cloning repos in it
+Git.prototype.allocateDir = function() {
+ var that = this;
+
+ if (this.tmpDir) return Promise();
+
+ return fs.tmpDir()
+ .then(function(dir) {
+ that.tmpDir = dir;
+ });
+};
+
// Clone a git repository if non existant
Git.prototype.clone = function(host, ref) {
var that = this;
- return Promise()
+ return this.allocateDir()
// Return or clone the git repo
.then(function() {
diff --git a/test/all.js b/test/all.js
index 242324bd4..35957b4e1 100644
--- a/test/all.js
+++ b/test/all.js
@@ -14,3 +14,4 @@ require('./template');
// Output
require('./output-json');
require('./assets-inliner');
+require('./website-theme');
diff --git a/test/website-theme.js b/test/website-theme.js
new file mode 100644
index 000000000..afbae5e29
--- /dev/null
+++ b/test/website-theme.js
@@ -0,0 +1,17 @@
+
+
+describe('Website Theming', function() {
+
+// Spec proposal:
+// - Plugins can "export" a theme
+// - Theme can be overrided in the book itself
+
+
+// Themes structures
+// - _layout/ (Folder containing all the templates)
+// - page.html (Template used for generating a page)
+// - _assets/ (Assets of the theme copied to the output)
+
+
+});
+
From 0e86cdeb4a3af445700e8e3ebbdb245e16581c17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 15:26:15 +0100
Subject: [PATCH 048/217] Add code blocks normalization
---
lib/page/html.js | 92 ++++++++++++++++++++++++++++++++---------------
lib/page/index.js | 12 ++++++-
2 files changed, 74 insertions(+), 30 deletions(-)
diff --git a/lib/page/html.js b/lib/page/html.js
index 255bf48b6..ea31a0682 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -25,6 +25,9 @@ function HTMLPipeline(htmlString, opts) {
// Output an image
onImage: _.identity,
+ // Syntax highlighting
+ onCodeBlock: _.identity,
+
// Output a svg, if returns null the svg is kept inlined
onOutputSVG: _.constant(null)
});
@@ -39,20 +42,28 @@ function HTMLPipeline(htmlString, opts) {
});
}
-// Normalize links
-HTMLPipeline.prototype.normalizeLinks = function() {
+// Transform a query of elements in the page
+HTMLPipeline.prototype._transform = function(query, fn) {
var that = this;
- this.$('a').each(function() {
- var $a = that.$(this);
+ var $elements = this.$(query);
+ return Promise.serie($elements, function(el) {
+ var $el = that.$(el);
+ return fn.call(that, $el);
+ });
+};
+
+// Normalize links
+HTMLPipeline.prototype.transformLinks = function() {
+ return this._transform('a', function($a) {
var href = $a.attr('href');
if (!href) return;
if (location.isAnchor(href)) {
// Don't "change" anchor links
} else if (location.isRelative(href)) {
- $a.attr('href', that.opts.onRelativeLink(href));
+ $a.attr('href', this.opts.onRelativeLink(href));
} else {
// External links
$a.attr('target', '_blank');
@@ -61,23 +72,50 @@ HTMLPipeline.prototype.normalizeLinks = function() {
};
// Normalize images
-HTMLPipeline.prototype.normalizeImages = function() {
- var that = this;
-
- var $imgs = this.$('img');
-
- return Promise.serie($imgs, function(img) {
- var $img = that.$(img);
-
- return Promise(that.opts.onImage($img.attr('src')))
+HTMLPipeline.prototype.transformImages = function() {
+ return this._transform('img', function($img) {
+ return Promise(this.opts.onImage($img.attr('src')))
.then(function(filename) {
$img.attr('src', filename);
});
});
};
+// Normalize code blocks
+HTMLPipeline.prototype.transformCodeBlocks = function() {
+ return this._transform('code', function($code) {
+ // Extract language
+ var lang = _.chain(
+ ($code.attr('class') || '').split(' ')
+ )
+ .map(function(cl) {
+ // Markdown
+ if (cl.search('lang-') === 0) return cl.slice('lang-'.length);
+
+ // Asciidoc
+ if (cl.search('language-') === 0) return cl.slice('language-'.length);
+
+ return null;
+ })
+ .compact()
+ .first()
+ .value();
+
+ var source = $code.text();
+
+ return Promise(this.opts.onCodeBlock(source, lang))
+ .then(function(blk) {
+ if (blk.html === false) {
+ $code.text(blk.body);
+ } else {
+ $code.html(blk.body);
+ }
+ });
+ });
+};
+
// Add ID to headings
-HTMLPipeline.prototype.addHeadingIDs = function() {
+HTMLPipeline.prototype.transformHeadings = function() {
var that = this;
this.$('h1,h2,h3,h4,h5,h6').each(function() {
@@ -90,23 +128,18 @@ HTMLPipeline.prototype.addHeadingIDs = function() {
};
// Outline SVG from the HML
-HTMLPipeline.prototype.outlineSVG = function() {
- var that = this;
-
- var $svgs = this.$('svg');
-
- return Promise.serie($svgs, function(svg) {
- var $svg = that.$(svg);
+HTMLPipeline.prototype.transformSvgs = function() {
+ return this._transform('svg', function($svg) {
var content = [
'',
- renderDOM(that.$, $svg)
+ renderDOM(this.$, $svg)
].join('\n');
- return Promise(that.opts.onOutputSVG(content))
+ return Promise(this.opts.onOutputSVG(content))
.then(function(filename) {
if (!filename) return;
- $svg.replaceWith(that.$(' ').attr('src', filename));
+ $svg.replaceWith(this.$(' ').attr('src', filename));
});
});
};
@@ -116,10 +149,11 @@ HTMLPipeline.prototype.output = function() {
var that = this;
return Promise()
- .then(this.normalizeLinks)
- .then(this.normalizeImages)
- .then(this.addHeadingIDs)
- .then(this.outlineSVG)
+ .then(this.transformLinks)
+ .then(this.transformImages)
+ .then(this.transformHeadings)
+ .then(this.transformCodeBlocks)
+ .then(this.transformSvgs)
.then(function() {
return renderDOM(that.$);
});
diff --git a/lib/page/index.js b/lib/page/index.js
index 77b995065..2a4553fbd 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -144,7 +144,17 @@ Page.prototype.parse = function(output) {
var pipelineOpts = {
onRelativeLink: _.partial(output.onRelativeLink, that),
onImage: _.partial(output.onOutputImage, that),
- onOutputSVG: _.partial(output.onOutputSVG, that)
+ onOutputSVG: _.partial(output.onOutputSVG, that),
+
+ // Use 'code' template block
+ onCodeBlock: function(source, lang) {
+ return output.template.applyBlock('code', {
+ body: source,
+ kwargs: {
+ language: lang
+ }
+ });
+ }
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
From 01a7c6f4e0e6518197b93ac350e73940197f22b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 17:41:55 +0100
Subject: [PATCH 049/217] Add help chapter about variables
---
docs/SUMMARY.md | 3 +++
docs/variables.md | 43 ++++++++++++++++++++++++++++++++++++++++++
lib/page/index.js | 21 +++++++++++++++------
lib/template/blocks.js | 2 +-
4 files changed, 62 insertions(+), 7 deletions(-)
create mode 100644 docs/variables.md
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index d8990733e..2816186d4 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -2,3 +2,6 @@
* [About this document](README.md)
* [Content References](conrefs.md)
+* [Variables](variables.md)
+ * [Filters](filters.md)
+
diff --git a/docs/variables.md b/docs/variables.md
new file mode 100644
index 000000000..35c7dd7e9
--- /dev/null
+++ b/docs/variables.md
@@ -0,0 +1,43 @@
+# Variables
+
+The following is a reference of the available data during book's parsing and theme generation.
+
+### Global Variables
+
+| Variable | Description |
+| -------- | ----------- |
+| `book` | Bookwide information + configuration settings from `book.json`. See below for details. |
+| `gitbook` | GitBook specific information |
+| `page` | Current page specific information |
+| `file` | File associated with the current page specific information |
+
+### Book Variables
+
+| Variable | Description |
+| -------- | ----------- |
+| `book.[CONFIGURATION_DATA]` | All the `variables` set via the `book.json` are available through the book variable. |
+
+### GitBook Variables
+
+| Variable | Description |
+| -------- | ----------- |
+| `gitbook.version` | Version of GitBook used to generate the book |
+
+### File Variables
+
+| Variable | Description |
+| -------- | ----------- |
+| `file.path` | The path to the raw page |
+| `file.mtime` | Modified Time, Time when file data last modified |
+
+#### Page Variables
+
+| Variable | Description |
+| -------- | ----------- |
+| `page.title` | Title of the page |
+| `page.previous` | Previous page in the Table of Contents (can be `null`) |
+| `page.next` | Next page in the Table of Contents (can be `null`) |
+
+
+
+
diff --git a/lib/page/index.js b/lib/page/index.js
index 2a4553fbd..d0574415c 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -103,6 +103,20 @@ Page.prototype.read = function() {
.then(this.update);
};
+// Return templating context for this page
+// This is used both for themes and page parsing
+Page.prototype.getContext = function() {
+ return {
+ file: {
+ path: this.path,
+ mtime: this.mtime
+ },
+ page: {
+ // todo
+ }
+ };
+};
+
// Parse the page and return its content
Page.prototype.parse = function(output) {
var that = this;
@@ -119,12 +133,7 @@ Page.prototype.parse = function(output) {
// Render template
.then(function() {
- return that.book.template.renderString(that.content, {
- file: {
- path: that.path,
- mtime: that.mtime
- }
- }, {
+ return that.book.template.renderString(that.content, that.getContext(), {
file: that.path
})
.then(that.update);
diff --git a/lib/template/blocks.js b/lib/template/blocks.js
index 92097a707..0e0464366 100644
--- a/lib/template/blocks.js
+++ b/lib/template/blocks.js
@@ -6,6 +6,6 @@ module.exports = {
html: _.identity,
// Highlight a code block
- // This block can be extent by plugins
+ // This block can be replaced by plugins
code: _.identity
};
From def115fe0f1df5c7c28694e99e096877a5c02e50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 19:07:04 +0100
Subject: [PATCH 050/217] Add base for templating documentation
---
cf33a0ef.svg | 259 ---------------------------------------------
docs/README.md | 2 +-
docs/SUMMARY.md | 5 +-
docs/templating.md | 8 ++
test.html | 16 ---
5 files changed, 13 insertions(+), 277 deletions(-)
delete mode 100644 cf33a0ef.svg
create mode 100644 docs/templating.md
delete mode 100644 test.html
diff --git a/cf33a0ef.svg b/cf33a0ef.svg
deleted file mode 100644
index e4b7e589a..000000000
--- a/cf33a0ef.svg
+++ /dev/null
@@ -1,259 +0,0 @@
-
-
- SVG Logo
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- SVG
-
-
-
-
-
-
-
-
diff --git a/docs/README.md b/docs/README.md
index 3590f45cd..98246f38f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,3 +1,3 @@
-# GitBook v{{ book.version }}: Technical documentation
+# GitBook v{{ book.version }}: Documentation
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 2816186d4..ad5b67ad8 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -1,7 +1,10 @@
# Summary
* [About this document](README.md)
+* [Pages and Summary](pages.md)
+* [Assets](assets.md)
+* [Glossary](glossary.md)
+* [Templating](templating.md)
* [Content References](conrefs.md)
* [Variables](variables.md)
* [Filters](filters.md)
-
diff --git a/docs/templating.md b/docs/templating.md
new file mode 100644
index 000000000..0c0f5afdf
--- /dev/null
+++ b/docs/templating.md
@@ -0,0 +1,8 @@
+# Templating
+
+GitBook uses the Nunjucks templating language to process pages and templates.
+
+The Nunjucks syntax is very similar to Jinja2 and Liquid.
+
+
+
diff --git a/test.html b/test.html
deleted file mode 100644
index 3dee7eff4..000000000
--- a/test.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
- The HTML5 Herald
-
-
-
-
-
-
-
-
-
\ No newline at end of file
From cfefa7d57992738373649dab16cbaf4754c3e5c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 19:27:58 +0100
Subject: [PATCH 051/217] Add .next and .prev to TOCArticle
---
lib/backbone/summary.js | 72 +++++++++++++++++++++++++++++++++++++++--
lib/page/index.js | 6 +++-
2 files changed, 74 insertions(+), 4 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index aa521041e..5775c8b6d 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -14,6 +14,7 @@ It's defined by a title, a reference, and children articles, the reference (ref)
function TOCArticle(summary, title, ref, articles, parent) {
this.summary = summary;
this.title = title;
+ this.parent = parent;
if (ref && location.isExternal(ref)) {
throw error.ParsingError(new Error('SUMMARY can only contains relative locations'));
@@ -50,6 +51,45 @@ 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 TOCArticle);
+};
+
+// Return next article in the TOC
+TOCArticle.prototype.next = function() {
+ var parentsArticles = this.parent.articles;
+ var pos = _.findIndex(parentsArticles, this);
+
+ if ((pos + 1) >= parentsArticles.length) {
+ if (this.hasParent()) {
+ return this.parent.next();
+ } else {
+ return null;
+ }
+ } else {
+ // next has the same parent
+ return parentsArticles[pos + 1];
+ }
+};
+
+// Return previous article in the TOC
+TOCArticle.prototype.prev = function() {
+ var parentsArticles = this.parent.articles;
+ var pos = _.findIndex(parentsArticles, this);
+
+ if ((pos - 1) < 0) {
+ if (this.hasParent()) {
+ return this.parent.prev();
+ } else {
+ return null;
+ }
+ } else {
+ // prev has the same parent
+ return parentsArticles[pos - 1];
+ }
+};
+
/*
A part of a ToC is a composed of a tree of articles.
*/
@@ -58,14 +98,17 @@ function TOCPart(summary, part) {
this.summary = summary;
this.articles = _.map(part.articles || part.chapters, function(article) {
- return new TOCArticle(that.summary, article.title, article.path, article.articles);
- });
+ return new TOCArticle(that.summary, article.title, article.path, article.articles, this);
+ }, this);
}
// Iterate over all entries of the part
TOCPart.prototype.walk = function(iter) {
_.each(this.articles, function(article) {
- iter(article);
+ if (iter(article) === false) {
+ return false;
+ }
+
article.walk(iter);
});
};
@@ -109,6 +152,29 @@ Summary.prototype.walk = function(iter) {
});
};
+// 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.getByPage = function(page) {
+ if (!_.isString(page)) page = page.path;
+
+ return this.find(function(article) {
+ return article.path == page;
+ });
+};
+
// Return the count of articles in the summary
Summary.prototype.count = function() {
return this._length;
diff --git a/lib/page/index.js b/lib/page/index.js
index d0574415c..6911ce284 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -106,13 +106,17 @@ Page.prototype.read = function() {
// Return templating context for this page
// This is used both for themes and page parsing
Page.prototype.getContext = function() {
+ var article = this.book.summary.find(this);
+
return {
file: {
path: this.path,
mtime: this.mtime
},
page: {
- // todo
+ title: article? article.title : null,
+ next: article? article.next() : null,
+ previous: article? article.prev() : null
}
};
};
From fef822145d43d80dbbefa8712eab88ad7044d6c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 22:49:55 +0100
Subject: [PATCH 052/217] Move conrefs to a separate mixin
---
lib/backbone/summary.js | 2 +-
lib/book.js | 4 ---
lib/output/base.js | 12 ++++++--
lib/output/conrefs.js | 49 +++++++++++++++++++++++++++++--
lib/output/json.js | 2 +-
lib/page/html.js | 8 +++--
lib/page/index.js | 6 ++--
lib/template/index.js | 2 +-
lib/template/loader.js | 65 +++++++++--------------------------------
lib/utils/git.js | 4 +--
package.json | 2 +-
test/all.js | 1 +
test/conrefs.js | 49 +++++++++++++++++++++++++++++++
test/page.js | 4 +--
test/template.js | 49 +++++--------------------------
15 files changed, 142 insertions(+), 117 deletions(-)
create mode 100644 test/conrefs.js
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 5775c8b6d..f4b9288b2 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -167,7 +167,7 @@ Summary.prototype.find = function(filter) {
};
// Return the first TOCArticle for a specific page (or path)
-Summary.prototype.getByPage = function(page) {
+Summary.prototype.getArticle = function(page) {
if (!_.isString(page)) page = page.path;
return this.find(function(article) {
diff --git a/lib/book.js b/lib/book.js
index 8c0d39b8d..cca1da5e6 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -10,7 +10,6 @@ var Glossary = require('./backbone/glossary');
var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
var Page = require('./page');
-var TemplateEngine = require('./template');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
var Promise = require('./utils/promise');
@@ -90,9 +89,6 @@ function Book(opts) {
// List of page in the book
this.pages = {};
- // Templating engine
- this.template = new TemplateEngine(this);
-
_.bindAll(this);
}
diff --git a/lib/output/base.js b/lib/output/base.js
index 8142761ae..fede3da66 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -1,5 +1,6 @@
var _ = require('lodash');
var Ignore = require('ignore');
+var path = require('path');
var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
@@ -107,7 +108,7 @@ Output.prototype.prepare = function() {
// Write a page (parsable file), ex: markdown, etc
Output.prototype.onPage = function(page) {
- return page.parse(this);
+ return page.toHTML(this);
};
// Copy an asset file (non-parsable), ex: images, etc
@@ -134,9 +135,14 @@ Output.prototype.onOutputImage = function(page, imgFile) {
return page.relative(imgFile);
};
-// By default don;t resolve conrefs
-Output.prototype.onResolveTemplate = function(from, to) {
+// Read a template by its source URL
+Output.prototype.onGetTemplate = function(sourceUrl) {
+ throw new Error('template not found '+sourceUrl);
+};
+// Generate a source URL for a template
+Output.prototype.onResolveTemplate = function(from, to) {
+ return path.resolve(path.dirname(from), to);
};
// Finish the generation
diff --git a/lib/output/conrefs.js b/lib/output/conrefs.js
index 2883c7b42..fb2d5bb65 100644
--- a/lib/output/conrefs.js
+++ b/lib/output/conrefs.js
@@ -1,7 +1,9 @@
var util = require('util');
+var path = require('path');
var Output = require('./base');
var Git = require('../utils/git');
+var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
/*
@@ -14,9 +16,52 @@ function ConrefsLoader() {
}
util.inherits(ConrefsLoader, Output);
-// Resolve an include in the template engine
-ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
+// Read a template by its source URL
+ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
+ var that = this;
+ return this.git.resolve(sourceURL)
+ .then(function(filepath) {
+ // Is local file
+ if (!filepath) {
+ filepath = that.book.resolve(sourceURL);
+ } else {
+ that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
+ }
+
+ // Read file from absolute path
+ return fs.readFile(filepath)
+ .then(function(source) {
+ return source.toString('utf8');
+ //return that.engine.interpolate(filepath, source);
+ })
+ .then(function(source) {
+ return {
+ src: source,
+ path: filepath
+ };
+ });
+ });
+};
+
+// Generate a source URL for a template
+ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
+ // If origin is in the book, we enforce result file to be in the book
+ if (this.book.isInBook(from)) {
+ return this.book.resolve(
+ this.book.relative(path.dirname(from)),
+ to
+ );
+ }
+
+ // If origin is in a git repository, we resolve file in the git repository
+ var gitRoot = this.git.resolveRoot(from);
+ if (gitRoot) {
+ return pathUtil.resolveInRoot(gitRoot, to);
+ }
+
+ // If origin is not in the book (include from a git content ref)
+ return path.resolve(path.dirname(from), to);
};
module.exports = ConrefsLoader;
diff --git a/lib/output/json.js b/lib/output/json.js
index 8cf23738f..4ad523165 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -17,7 +17,7 @@ JSONOutput.prototype.onPage = function(page) {
var that = this;
// Parse the page
- return page.parse(this)
+ return page.toHTML(this)
// Write as json
.then(function() {
diff --git a/lib/page/html.js b/lib/page/html.js
index ea31a0682..71b29b2c3 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -129,17 +129,19 @@ HTMLPipeline.prototype.transformHeadings = function() {
// Outline SVG from the HML
HTMLPipeline.prototype.transformSvgs = function() {
+ var that = this;
+
return this._transform('svg', function($svg) {
var content = [
'',
- renderDOM(this.$, $svg)
+ renderDOM(that.$, $svg)
].join('\n');
- return Promise(this.opts.onOutputSVG(content))
+ return Promise(that.opts.onOutputSVG(content))
.then(function(filename) {
if (!filename) return;
- $svg.replaceWith(this.$(' ').attr('src', filename));
+ $svg.replaceWith(that.$(' ').attr('src', filename));
});
});
};
diff --git a/lib/page/index.js b/lib/page/index.js
index 6911ce284..51416f0bd 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -106,7 +106,7 @@ Page.prototype.read = function() {
// Return templating context for this page
// This is used both for themes and page parsing
Page.prototype.getContext = function() {
- var article = this.book.summary.find(this);
+ var article = this.book.summary.getArticle(this);
return {
file: {
@@ -122,7 +122,7 @@ Page.prototype.getContext = function() {
};
// Parse the page and return its content
-Page.prototype.parse = function(output) {
+Page.prototype.toHTML = function(output) {
var that = this;
this.log.debug.ln('start parsing file', this.path);
@@ -137,7 +137,7 @@ Page.prototype.parse = function(output) {
// Render template
.then(function() {
- return that.book.template.renderString(that.content, that.getContext(), {
+ return output.template.renderString(that.content, that.getContext(), {
file: that.path
})
.then(that.update);
diff --git a/lib/template/index.js b/lib/template/index.js
index 3f74267e3..6074f51fb 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -27,7 +27,7 @@ function TemplateEngine(output) {
this.log = this.book.log;
// Create file loader
- this.loader = new Loader(this);
+ this.loader = new Loader(output);
// Create nunjucks instance
this.env = new nunjucks.Environment(
diff --git a/lib/template/loader.js b/lib/template/loader.js
index 5d30fb26d..6b54015ac 100644
--- a/lib/template/loader.js
+++ b/lib/template/loader.js
@@ -1,74 +1,35 @@
-var path = require('path');
var nunjucks = require('nunjucks');
+var location = require('../utils/location');
-var Git = require('../utils/git');
-var pathUtil = require('../utils/path');
+/*
+Simple nunjucks loader which is passing the reponsability to the Output
+*/
-// The loader should handle relative and git url
var Loader = nunjucks.Loader.extend({
async: true,
- init: function(engine, opts) {
- this.engine = engine;
- this.book = engine.book;
- this.fs = engine.book.fs;
-
- this.git = new Git(this.fs.tmpdir());
+ init: function(output, opts) {
+ this.output = output;
},
getSource: function(sourceURL, callback) {
- var that = this;
+ this.output.onGetTemplate(sourceURL)
+ .then(function(out) {
+ // We disable cache since content is modified (shortcuts, ...)
+ out.noCache = true;
- this.git.resolve(sourceURL)
- .then(function(filepath) {
- // Is local file
- if (!filepath) {
- filepath = that.book.resolve(sourceURL);
- } else {
- that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
- }
-
- // Read file from absolute path
- return that.fs.readAsString(filepath)
- .then(function(source) {
- return that.engine.interpolate(filepath, source);
- })
- .then(function(source) {
- return {
- src: source,
- path: filepath,
-
- // We disable cache since content is modified (shortcuts, ...)
- noCache: true
- };
- });
+ return out;
})
.nodeify(callback);
},
resolve: function(from, to) {
- // If origin is in the book, we enforce result file to be in the book
- if (this.book.isInBook(from)) {
- return this.book.resolve(
- this.book.relative(path.dirname(from)),
- to
- );
- }
-
- // If origin is in a git repository, we resolve file in the git repository
- var gitRoot = this.git.resolveRoot(from);
- if (gitRoot) {
- return pathUtil.resolveInRoot(gitRoot, to);
- }
-
- // If origin is not in the book (include from a git content ref)
- return path.resolve(path.dirname(from), to);
+ return this.output.onResolveTemplate(from, to);
},
// Handle all files as relative, so that nunjucks pass responsability to 'resolve'
- // Only git urls are considered as absolute
isRelative: function(filename) {
- return !Git.isUrl(filename);
+ return location.isRelative(filename);
}
});
diff --git a/lib/utils/git.js b/lib/utils/git.js
index fc75ee375..52b109666 100644
--- a/lib/utils/git.js
+++ b/lib/utils/git.js
@@ -10,7 +10,7 @@ var fs = require('./fs');
var GIT_PREFIX = 'git+';
-function Git(tmpDir) {
+function Git() {
this.tmpDir;
this.cloned = {};
}
@@ -84,7 +84,7 @@ Git.prototype.resolveRoot = function(filepath) {
var relativeToGit, repoId;
// No git repo cloned, or file is not in a git repository
- if (!pathUtil.isInRoot(this.tmpDir, filepath)) return null;
+ if (!this.tmpDir || !pathUtil.isInRoot(this.tmpDir, filepath)) return null;
// Extract first directory (is the repo id)
relativeToGit = path.relative(this.tmpDir, filepath);
diff --git a/package.json b/package.json
index 1d0b52053..69e91b798 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
"gitbook-markdown-css": "1.0.1"
},
"scripts": {
- "test": "node_modules/.bin/mocha --reporter spec --timeout 15000 ./test/all.js"
+ "test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js"
},
"repository": {
"type": "git",
diff --git a/test/all.js b/test/all.js
index 35957b4e1..2c871559a 100644
--- a/test/all.js
+++ b/test/all.js
@@ -10,6 +10,7 @@ require('./parse');
require('./git');
require('./template');
+require('./conrefs');
// Output
require('./output-json');
diff --git a/test/conrefs.js b/test/conrefs.js
new file mode 100644
index 000000000..e75055cdb
--- /dev/null
+++ b/test/conrefs.js
@@ -0,0 +1,49 @@
+var mock = require('./mock');
+var ConrefsLoader = require('../lib/output/conrefs');
+
+
+describe('Conrefs Loader', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook(ConrefsLoader, {
+ 'test.md': 'World'
+ })
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+
+ it('should include a local file', function() {
+ return output.template.renderString('Hello {% include "./test.md" %}')
+ .should.be.fulfilledWith('Hello World');
+ });
+
+ it('should include a git url', function() {
+ return output.template.renderString('Hello {% include "./test.md" %}')
+ .should.be.fulfilledWith('Hello World');
+ });
+
+ it('should reject file out of scope', function() {
+ return output.template.renderString('Hello {% include "../test.md" %}')
+ .should.be.rejected();
+ });
+
+ describe('Git Urls', function() {
+ it('should include a file from a git repo', function() {
+ return output.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}')
+ .should.be.fulfilledWith('Hello from git');
+ });
+
+ it('should handle deep inclusion (1)', function() {
+ return output.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}')
+ .should.be.fulfilledWith('First Hello. Hello from git');
+ });
+
+ it('should handle deep inclusion (2)', function() {
+ return output.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}')
+ .should.be.fulfilledWith('First Hello. Hello from git');
+ });
+ });
+});
diff --git a/test/page.js b/test/page.js
index d844d1331..8dbfe8bd5 100644
--- a/test/page.js
+++ b/test/page.js
@@ -54,7 +54,7 @@ describe('Page', function() {
it('should add a default ID to headings', function() {
var page = book.addPage('heading.md');
- return page.parse(output)
+ return page.toHTML(output)
.then(function() {
page.content.should.be.html({
'h1#hello': {
@@ -73,7 +73,7 @@ describe('Page', function() {
before(function() {
page = book.addPage('links.md');
- return page.parse(output);
+ return page.toHTML(output);
});
it('should replace links to page to .html', function() {
diff --git a/test/template.js b/test/template.js
index f338a848e..536043d27 100644
--- a/test/template.js
+++ b/test/template.js
@@ -1,63 +1,28 @@
var mock = require('./mock');
var pkg = require('../package.json');
+var Output = require('../lib/output/base');
describe('Template', function() {
- var book;
+ var output;
before(function() {
- return mock.setupDefaultBook({
+ return mock.outputDefaultBook(Output, {
'test.md': 'World'
})
- .then(function(_book) {
- book = _book;
- return book.parse();
+ .then(function(_output) {
+ output = _output;
});
});
describe('.renderString', function() {
it('should render a simple string', function() {
- return book.template.renderString('Hello World')
+ return output.template.renderString('Hello World')
.should.be.fulfilledWith('Hello World');
});
it('should render with variable', function() {
- return book.template.renderString('Version is {{ gitbook.version }}')
+ return output.template.renderString('Version is {{ gitbook.version }}')
.should.be.fulfilledWith('Version is '+pkg.version);
});
});
-
- describe('Conrefs Loader', function() {
- it('should include a local file', function() {
- return book.template.renderString('Hello {% include "./test.md" %}')
- .should.be.fulfilledWith('Hello World');
- });
-
- it('should include a git url', function() {
- return book.template.renderString('Hello {% include "./test.md" %}')
- .should.be.fulfilledWith('Hello World');
- });
-
- it('should reject file out of scope', function() {
- return book.template.renderString('Hello {% include "../test.md" %}')
- .should.be.rejected();
- });
-
- describe('Git Urls', function() {
- it('should include a file from a git repo', function() {
- return book.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}')
- .should.be.fulfilledWith('Hello from git');
- });
-
- it('should handle deep inclusion (1)', function() {
- return book.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}')
- .should.be.fulfilledWith('First Hello. Hello from git');
- });
-
- it('should handle deep inclusion (2)', function() {
- return book.template.renderString('{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}')
- .should.be.fulfilledWith('First Hello. Hello from git');
- });
- });
- });
-
});
From 8628c87be479015b204520bb058b0cc823859d70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 22:55:48 +0100
Subject: [PATCH 053/217] Interpolate template before running it
---
lib/output/conrefs.js | 6 +-----
lib/template/index.js | 2 +-
lib/template/loader.js | 10 ++++++++--
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/lib/output/conrefs.js b/lib/output/conrefs.js
index fb2d5bb65..98230d99f 100644
--- a/lib/output/conrefs.js
+++ b/lib/output/conrefs.js
@@ -31,13 +31,9 @@ ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
// Read file from absolute path
return fs.readFile(filepath)
- .then(function(source) {
- return source.toString('utf8');
- //return that.engine.interpolate(filepath, source);
- })
.then(function(source) {
return {
- src: source,
+ src: source.toString('utf8'),
path: filepath
};
});
diff --git a/lib/template/index.js b/lib/template/index.js
index 6074f51fb..3f74267e3 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -27,7 +27,7 @@ function TemplateEngine(output) {
this.log = this.book.log;
// Create file loader
- this.loader = new Loader(output);
+ this.loader = new Loader(this);
// Create nunjucks instance
this.env = new nunjucks.Environment(
diff --git a/lib/template/loader.js b/lib/template/loader.js
index 6b54015ac..23d179a83 100644
--- a/lib/template/loader.js
+++ b/lib/template/loader.js
@@ -8,16 +8,22 @@ Simple nunjucks loader which is passing the reponsability to the Output
var Loader = nunjucks.Loader.extend({
async: true,
- init: function(output, opts) {
- this.output = output;
+ init: function(engine, opts) {
+ this.engine = engine;
+ this.output = engine.output;
},
getSource: function(sourceURL, callback) {
+ var that = this;
+
this.output.onGetTemplate(sourceURL)
.then(function(out) {
// We disable cache since content is modified (shortcuts, ...)
out.noCache = true;
+ // Transform template before runnign it
+ out.source = that.engine.interpolate(out.path, out.source);
+
return out;
})
.nodeify(callback);
From d6bd4a0d67dd2f61e667aefebf96e279f607ce54 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 14 Feb 2016 23:05:37 +0100
Subject: [PATCH 054/217] Add test for file variables in templating
---
lib/page/index.js | 5 +++++
test/all.js | 4 +++-
test/page.js | 20 +++++++++++++++++++-
3 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/lib/page/index.js b/lib/page/index.js
index 51416f0bd..54255ddf9 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -173,6 +173,11 @@ Page.prototype.toHTML = function(output) {
return pipeline.output()
.then(that.update);
+ })
+
+ // Return content itself
+ .then(function() {
+ return that.content;
});
};
diff --git a/test/all.js b/test/all.js
index 2c871559a..fed3f61a4 100644
--- a/test/all.js
+++ b/test/all.js
@@ -5,13 +5,15 @@ require('./readme');
require('./summary');
require('./glossary');
require('./langs');
-require('./page');
require('./parse');
require('./git');
require('./template');
require('./conrefs');
+// Page and HTML generation
+require('./page');
+
// Output
require('./output-json');
require('./assets-inliner');
diff --git a/test/page.js b/test/page.js
index 8dbfe8bd5..9932e3884 100644
--- a/test/page.js
+++ b/test/page.js
@@ -8,7 +8,9 @@ describe('Page', function() {
return mock.setupDefaultBook({
'heading.md': '# Hello\n\n## World',
'links.md': '[link](hello.md) [readme](README.md)',
- 'folder/paths.md': ''
+ 'folder/paths.md': '',
+ 'variables/file/mtime.md': '{{ file.mtime }}',
+ 'variables/file/path.md': '{{ file.path }}'
})
.then(function(_book) {
book = _book;
@@ -93,4 +95,20 @@ describe('Page', function() {
});
});
+
+ describe('Templating Context', function() {
+ it('should set file.mtime', function() {
+ var page = book.addPage('variables/file/mtime.md');
+ return page.toHTML(output)
+ .then(function(content) {
+ content.should.endWith('(CET)
\n');
+ });
+ });
+
+ it('should set file.[path]', function() {
+ var page = book.addPage('variables/file/path.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('variables/file/path.md
\n');
+ });
+ });
});
\ No newline at end of file
From d7077fc7db130d91f117dbf7ef01ec6ff5c6fb8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 15 Feb 2016 09:44:05 +0100
Subject: [PATCH 055/217] Add test for page.title variable
---
test/mock.js | 12 +++++++++---
test/page.js | 21 ++++++++++++++++-----
2 files changed, 25 insertions(+), 8 deletions(-)
diff --git a/test/mock.js b/test/mock.js
index 127e35cb8..ce4dca5df 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -49,15 +49,21 @@ function setupBook(files, opts) {
}
// Setup a book with default README/SUMMARY
-function setupDefaultBook(files, opts) {
+function setupDefaultBook(files, summary, opts) {
+ var summaryContent = '# Summary \n\n' +
+ _.map(summary, function(article) {
+ return '* [' + article.title +'](' + article.path + ')';
+ })
+ .join('\n');
+
return setupBook(_.defaults(files || {}, {
'README.md': 'Hello',
- 'SUMMARY.md': '# Summary'
+ 'SUMMARY.md': summaryContent
}), opts);
}
// Output a book with a specific generator
-function outputDefaultBook(Output, files, opts) {
+function outputDefaultBook(Output, files, summary, opts) {
return setupDefaultBook(files, opts)
.then(function(book) {
// Parse the book
diff --git a/test/page.js b/test/page.js
index 9932e3884..a9b798560 100644
--- a/test/page.js
+++ b/test/page.js
@@ -10,8 +10,14 @@ describe('Page', function() {
'links.md': '[link](hello.md) [readme](README.md)',
'folder/paths.md': '',
'variables/file/mtime.md': '{{ file.mtime }}',
- 'variables/file/path.md': '{{ file.path }}'
- })
+ 'variables/file/path.md': '{{ file.path }}',
+ 'variables/page/title.md': '{{ page.title }}'
+ }, [
+ {
+ title: 'Test Variables',
+ path: 'variables/page/title.md'
+ }
+ ])
.then(function(_book) {
book = _book;
output = new Output(book);
@@ -95,7 +101,6 @@ describe('Page', function() {
});
});
-
describe('Templating Context', function() {
it('should set file.mtime', function() {
var page = book.addPage('variables/file/mtime.md');
@@ -105,10 +110,16 @@ describe('Page', function() {
});
});
- it('should set file.[path]', function() {
+ it('should set file.path', function() {
var page = book.addPage('variables/file/path.md');
return page.toHTML(output)
.should.be.fulfilledWith('variables/file/path.md
\n');
});
+
+ it('should set page.title when page is in summary', function() {
+ var page = book.getPage('variables/page/title.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('Test Variables
\n');
+ });
});
-});
\ No newline at end of file
+});
From 1b19c6660303cb6ce98ac0ad76580f66b52985f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 15 Feb 2016 09:55:12 +0100
Subject: [PATCH 056/217] Complete page.previous and page.next variables
---
lib/backbone/summary.js | 8 ++++++++
lib/page/index.js | 6 ++++--
test/page.js | 24 +++++++++++++++++++++++-
3 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index f4b9288b2..cef3ae901 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -41,6 +41,14 @@ TOCArticle.prototype.walk = function(iter) {
});
};
+// Return templating context for an article
+TOCArticle.prototype.getContext = function() {
+ return {
+ title: this.title,
+ path: this.path
+ };
+};
+
// Return true if is pointing to a file
TOCArticle.prototype.hasLocation = function() {
return Boolean(this.path);
diff --git a/lib/page/index.js b/lib/page/index.js
index 54255ddf9..b3ff00f6e 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -107,6 +107,8 @@ Page.prototype.read = function() {
// This is used both for themes and page parsing
Page.prototype.getContext = function() {
var article = this.book.summary.getArticle(this);
+ var next = article? article.next() : null;
+ var prev = article? article.prev() : null;
return {
file: {
@@ -115,8 +117,8 @@ Page.prototype.getContext = function() {
},
page: {
title: article? article.title : null,
- next: article? article.next() : null,
- previous: article? article.prev() : null
+ next: next? next.getContext() : null,
+ previous: prev? prev.getContext() : null
}
};
};
diff --git a/test/page.js b/test/page.js
index a9b798560..fb3e3e502 100644
--- a/test/page.js
+++ b/test/page.js
@@ -11,11 +11,21 @@ describe('Page', function() {
'folder/paths.md': '',
'variables/file/mtime.md': '{{ file.mtime }}',
'variables/file/path.md': '{{ file.path }}',
- 'variables/page/title.md': '{{ page.title }}'
+ 'variables/page/title.md': '{{ page.title }}',
+ 'variables/page/previous.md': '{{ page.previous.title }} {{ page.previous.path }}',
+ 'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}'
}, [
+ {
+ title: 'Test page.next',
+ path: 'variables/page/next.md'
+ },
{
title: 'Test Variables',
path: 'variables/page/title.md'
+ },
+ {
+ title: 'Test page.previous',
+ path: 'variables/page/previous.md'
}
])
.then(function(_book) {
@@ -121,5 +131,17 @@ describe('Page', function() {
return page.toHTML(output)
.should.be.fulfilledWith('Test Variables
\n');
});
+
+ it('should set page.previous when possible', function() {
+ var page = book.getPage('variables/page/previous.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('Test Variables variables/page/title.md
\n');
+ });
+
+ it('should set page.next when possible', function() {
+ var page = book.getPage('variables/page/next.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('Test Variables variables/page/title.md
\n');
+ });
});
});
From 354fe1274242bef87b739dbe43cdb634ea573662 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 15 Feb 2016 14:07:26 +0100
Subject: [PATCH 057/217] Fix addBlock for template engine
Add test for code block
---
lib/book.js | 2 +-
lib/output/ebook.js | 2 +
lib/output/website/index.js | 2 +
lib/page/index.js | 8 +++-
lib/template/index.js | 83 +++++++++++++++++++++++++++++++++++--
lib/utils/error.js | 18 +++++++-
lib/{ => utils}/logger.js | 2 +-
package.json | 5 ++-
test/page.js | 34 +++++++++++++++
test/template.js | 24 +++++++++--
10 files changed, 168 insertions(+), 12 deletions(-)
rename lib/{ => utils}/logger.js (97%)
diff --git a/lib/book.js b/lib/book.js
index cca1da5e6..173e9cce6 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -3,7 +3,6 @@ var path = require('path');
var Ignore = require('ignore');
var parsers = require('gitbook-parsers');
-var Logger = require('./logger');
var Config = require('./config');
var Readme = require('./backbone/readme');
var Glossary = require('./backbone/glossary');
@@ -13,6 +12,7 @@ var Page = require('./page');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
var Promise = require('./utils/promise');
+var Logger = require('./utils/logger');
/*
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index b968006e4..b4cd55088 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -10,5 +10,7 @@ function EbookOutput() {
util.inherits(EbookOutput, AssetsInliner);
util.inherits(EbookOutput, WebsiteOutput);
+EbookOutput.prototype.name = 'ebook';
+
module.exports = EbookOutput;
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index a35cafe42..b268eec65 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -8,6 +8,8 @@ function WebsiteOutput() {
}
util.inherits(WebsiteOutput, FolderOutput);
+WebsiteOutput.prototype.name = 'ebook';
+
// Write a page (parsable file)
WebsiteOutput.prototype.onPage = function(page) {
diff --git a/lib/page/index.js b/lib/page/index.js
index b3ff00f6e..342830a45 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -139,7 +139,7 @@ Page.prototype.toHTML = function(output) {
// Render template
.then(function() {
- return output.template.renderString(that.content, that.getContext(), {
+ return output.template.render(that.content, that.getContext(), {
file: that.path
})
.then(that.update);
@@ -154,6 +154,12 @@ Page.prototype.toHTML = function(output) {
});
})
+ // Post process templating
+ .then(function() {
+ return output.template.postProcess(that.content)
+ .then(that.update);
+ })
+
// Normalize HTML output
.then(function() {
var pipelineOpts = {
diff --git a/lib/template/index.js b/lib/template/index.js
index 3f74267e3..826657212 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -64,6 +64,20 @@ function TemplateEngine(output) {
this.addBlocks(defaultBlocks);
}
+// Bind a function to a context
+// Filters and blocks are binded to this context
+TemplateEngine.prototype.bindContext = function(func) {
+ var ctx = {
+ ctx: this.ctx,
+ book: this.book,
+ output: this.output
+ };
+
+ error.deprecateField(ctx, 'generator', this.output.name, 'generator property is deprecated, use output.name instead');
+
+ return _.bind(func, ctx);
+};
+
// Interpolate a string content to replace shortcuts according to the filetype
TemplateEngine.prototype.interpolate = function(filepath, source) {
var parser = parsers.get(path.extname(filepath));
@@ -240,7 +254,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
}, context);
})
- // process the block returned
+ // Process the block returned
.then(that.processBlock)
.nodeify(callback);
};
@@ -303,9 +317,30 @@ TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
else return normBlockResult(r);
};
+// Process the result of block in a context
+TemplateEngine.prototype.processBlock = function(blk) {
+ blk = _.defaults(blk, {
+ parse: false,
+ post: undefined
+ });
+ blk.id = _.uniqueId('blk');
-// Render a string
-TemplateEngine.prototype.renderString = function(content, context, options) {
+ var toAdd = (!blk.parse) || (blk.post !== undefined);
+
+ // Add to global map
+ if (toAdd) this.blockBodies[blk.id] = blk;
+
+ // Parsable block, just return it
+ if (blk.parse) {
+ return blk.body;
+ }
+
+ // Return it as a position marker
+ return '@%@'+blk.id+'@%@';
+};
+
+// Render a string (without post processing)
+TemplateEngine.prototype.render = function(content, context, options) {
options = _.defaults(options || {}, {
path: null
});
@@ -345,6 +380,12 @@ TemplateEngine.prototype.renderString = function(content, context, options) {
});
};
+// Render a string with post-processing
+TemplateEngine.prototype.renderString = function(content, context, options) {
+ return this.render(content, context, options)
+ .then(this.postProcess);
+};
+
// Apply a shortcut to a string
TemplateEngine.prototype.applyShortcut = function(content, shortcut) {
var regex = new RegExp(
@@ -356,6 +397,21 @@ TemplateEngine.prototype.applyShortcut = function(content, shortcut) {
});
};
+// Replace position markers of blocks by body after processing
+// This is done to avoid that markdown/asciidoc processer parse the block content
+TemplateEngine.prototype.replaceBlocks = function(content) {
+ var that = this;
+
+ return content.replace(/\@\%\@([\s\S]+?)\@\%\@/g, function(match, key) {
+ var blk = that.blockBodies[key];
+ if (!blk) return match;
+
+ var body = blk.body;
+
+ return body;
+ });
+};
+
// Apply all shortcuts to a template
TemplateEngine.prototype.applyShortcuts = function(type, content) {
return _.chain(this.shortcuts)
@@ -367,4 +423,25 @@ TemplateEngine.prototype.applyShortcuts = function(type, content) {
};
+// Post process content
+TemplateEngine.prototype.postProcess = function(content) {
+ var that = this;
+
+ return Promise(content)
+ .then(that.replaceBlocks)
+ .then(function(_content) {
+ return Promise.serie(that.blockBodies, function(blk, blkId) {
+ return Promise()
+ .then(function() {
+ if (!blk.post) return;
+ return blk.post();
+ })
+ .then(function() {
+ delete that.blockBodies[blkId];
+ });
+ })
+ .thenResolve(_content);
+ });
+};
+
module.exports = TemplateEngine;
diff --git a/lib/utils/error.js b/lib/utils/error.js
index 883a7c61e..c1abebd68 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -1,6 +1,11 @@
var _ = require('lodash');
var TypedError = require('error/typed');
var WrappedError = require('error/wrapped');
+var deprecated = require('deprecated');
+
+var Logger = require('./logger');
+
+var log = new Logger();
// Enforce as an Error object, and cleanup message
function enforce(err) {
@@ -43,6 +48,14 @@ var TemplateError = WrappedError({
filename: null
});
+// Deprecate methods/fields
+function deprecateMethod(fn, msg) {
+ return deprecated.method(msg, log.warn.ln, fn);
+}
+function deprecateField(obj, prop, value, msg) {
+ return deprecated.field(msg, log.warn.ln, obj, prop, value);
+}
+
module.exports = {
enforce: enforce,
@@ -52,5 +65,8 @@ module.exports = {
FileNotFoundError: FileNotFoundError,
FileOutOfScopeError: FileOutOfScopeError,
- TemplateError: TemplateError
+ TemplateError: TemplateError,
+
+ deprecateMethod: deprecateMethod,
+ deprecateField: deprecateField
};
diff --git a/lib/logger.js b/lib/utils/logger.js
similarity index 97%
rename from lib/logger.js
rename to lib/utils/logger.js
index 74281802c..9dda57d48 100644
--- a/lib/logger.js
+++ b/lib/utils/logger.js
@@ -20,7 +20,7 @@ var COLORS = {
function Logger(write, logLevel) {
if (!(this instanceof Logger)) return new Logger(write, logLevel);
- this._write = write;
+ this._write = write || function(msg) { process.stdout.write(msg); };
this.lastChar = '\n';
// Define log level
diff --git a/package.json b/package.json
index 69e91b798..3eb60019a 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
"gitbook-plugin-fontsettings": "1.0.2",
- "nunjucks": "2.2.0",
+ "nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
"i18n": "0.5.0",
@@ -43,7 +43,8 @@
"merge-defaults": "0.2.1",
"github-slugid": "1.0.0",
"destroy": "1.0.4",
- "ignore": "2.2.19"
+ "ignore": "2.2.19",
+ "deprecated": "0.0.1"
},
"devDependencies": {
"eslint": "1.5.0",
diff --git a/test/page.js b/test/page.js
index fb3e3e502..b94751999 100644
--- a/test/page.js
+++ b/test/page.js
@@ -8,7 +8,13 @@ describe('Page', function() {
return mock.setupDefaultBook({
'heading.md': '# Hello\n\n## World',
'links.md': '[link](hello.md) [readme](README.md)',
+
+ 'codes/simple.md': '```hello world```',
+ 'codes/lang.md': '```js\nhello world\n```',
+ 'codes/lang.adoc': '```js\nhello world\n```',
+
'folder/paths.md': '',
+
'variables/file/mtime.md': '{{ file.mtime }}',
'variables/file/path.md': '{{ file.path }}',
'variables/page/title.md': '{{ page.title }}',
@@ -86,6 +92,34 @@ describe('Page', function() {
});
});
+ describe('Code Blocks', function() {
+ var page;
+
+ before(function() {
+ output.template.addBlock('code', function(blk) {
+ return (blk.kwargs.language || '') + blk.body + 'test';
+ });
+ });
+
+ it('should apply "code" block', function() {
+ page = book.addPage('codes/simple.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('hello worldtest
\n');
+ });
+
+ it('should add language as kwargs', function() {
+ page = book.addPage('codes/lang.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('jshello world\ntest \n');
+ });
+
+ it('should add language as kwargs (asciidoc)', function() {
+ page = book.addPage('codes/lang.adoc');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('');
+ });
+ });
+
describe('Links', function() {
var page;
diff --git a/test/template.js b/test/template.js
index 536043d27..d12a6415f 100644
--- a/test/template.js
+++ b/test/template.js
@@ -6,9 +6,7 @@ describe('Template', function() {
var output;
before(function() {
- return mock.outputDefaultBook(Output, {
- 'test.md': 'World'
- })
+ return mock.outputDefaultBook(Output, {})
.then(function(_output) {
output = _output;
});
@@ -25,4 +23,24 @@ describe('Template', function() {
.should.be.fulfilledWith('Version is '+pkg.version);
});
});
+
+ describe('Blocks', function() {
+ it('should correctly add a block', function() {
+ output.template.addBlock('sayhello', function(blk) {
+ return 'Hello ' + blk.body + '!';
+ });
+
+ return output.template.renderString('{% sayhello %}World{% endsayhello %}')
+ .should.be.fulfilledWith('Hello World!');
+ });
+
+ it('should correctly add a block with kwargs', function() {
+ output.template.addBlock('sayhello_kwargs', function(blk) {
+ return 'Hello ' + blk.kwargs.name + '!';
+ });
+
+ return output.template.renderString('{% sayhello_kwargs name="World" %}{% endsayhello_kwargs %}')
+ .should.be.fulfilledWith('Hello World!');
+ });
+ });
});
From 1141d34f07ca4a9937dd4f43b43e5faa01da832c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 15 Feb 2016 14:19:30 +0100
Subject: [PATCH 058/217] Add tests for args in blocks
---
test/template.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/test/template.js b/test/template.js
index d12a6415f..a76fe9377 100644
--- a/test/template.js
+++ b/test/template.js
@@ -34,6 +34,15 @@ describe('Template', function() {
.should.be.fulfilledWith('Hello World!');
});
+ it('should correctly add a block with args', function() {
+ output.template.addBlock('sayhello_args', function(blk) {
+ return 'Hello ' + blk.args[0] + '!';
+ });
+
+ return output.template.renderString('{% sayhello_args "World" %}{% endsayhello_args %}')
+ .should.be.fulfilledWith('Hello World!');
+ });
+
it('should correctly add a block with kwargs', function() {
output.template.addBlock('sayhello_kwargs', function(blk) {
return 'Hello ' + blk.kwargs.name + '!';
From e3c92cce77432849de5b9249b6ffb99045a31314 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Mon, 15 Feb 2016 16:24:13 +0100
Subject: [PATCH 059/217] Add name to json and website build
And inherit from conrefsLoader
---
lib/output/json.js | 12 ++++++------
lib/output/website/index.js | 8 ++++----
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/lib/output/json.js b/lib/output/json.js
index 4ad523165..a77b45a46 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,16 +1,16 @@
var util = require('util');
-var FolderOutput = require('./folder');
+var ConrefsLoader = require('./conrefs');
var gitbook = require('../gitbook');
function JSONOutput() {
- FolderOutput.apply(this, arguments);
+ ConrefsLoader.apply(this, arguments);
}
-util.inherits(JSONOutput, FolderOutput);
+util.inherits(JSONOutput, ConrefsLoader);
+
+JSONOutput.prototype.name = 'json';
// Don't copy asset on JSON output
-JSONOutput.prototype.onAsset = function(filename) {
-
-};
+JSONOutput.prototype.onAsset = function(filename) {};
// Write a page (parsable file)
JSONOutput.prototype.onPage = function(page) {
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index b268eec65..7322a578d 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -1,14 +1,14 @@
var util = require('util');
-var FolderOutput = require('../base');
+var ConrefsLoader = require('../conrefs');
var Theme = require('./theme');
function WebsiteOutput() {
- FolderOutput.apply(this, arguments);
+ ConrefsLoader.apply(this, arguments);
}
-util.inherits(WebsiteOutput, FolderOutput);
+util.inherits(WebsiteOutput, ConrefsLoader);
-WebsiteOutput.prototype.name = 'ebook';
+WebsiteOutput.prototype.name = 'website';
// Write a page (parsable file)
WebsiteOutput.prototype.onPage = function(page) {
From b4fcd2da14159106cfe1911ba6321e112ffb9722 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Mon, 15 Feb 2016 21:14:25 +0100
Subject: [PATCH 060/217] Plugins should be relative to output
---
lib/plugins/index.js | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 33a2047ae..f31f1b229 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -10,8 +10,9 @@ PluginsManager is an interface to work with multiple plugins at once:
- Call hooks for all plugins, etc
*/
-function PluginsManager(book) {
- this.book = book;
+function PluginsManager(output) {
+ this.output = output;
+ this.book = output.book;
this.plugins = [];
}
@@ -56,6 +57,4 @@ PluginsManager.prototype.load = function(name) {
});
};
-
-
module.exports = PluginsManager;
From 2375296e67cf813db2076877ba64c0d3dfae2b3f Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Mon, 15 Feb 2016 22:39:26 +0100
Subject: [PATCH 061/217] Add base _setup for pluginslist
---
lib/plugins/index.js | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index f31f1b229..ac595ac69 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -52,9 +52,14 @@ PluginsManager.prototype.load = function(name) {
.thenResolve(plugin);
})
- .then(function(plugin) {
- that.plugins.push(plugin);
- });
+ // Setup the plugin
+ .then(this._setup);
+};
+
+// Setup a plugin
+// Register its filter, blocks, etc
+PluginsManager.prototype._setup = function(plugin) {
+
};
module.exports = PluginsManager;
From 327d0bf5c52982a5a20967c8435aff9b60ed509d Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Tue, 16 Feb 2016 15:42:24 +0100
Subject: [PATCH 062/217] Validate plugin after loading
---
lib/plugins/plugin.js | 33 ++++++++++++++++++++++++++++-----
1 file changed, 28 insertions(+), 5 deletions(-)
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index d4d2b231c..05fa8ca04 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -1,7 +1,9 @@
+var _ = require('lodash');
var path = require('path');
var resolve = require('resolve');
var Promise = require('../utils/promise');
+var gitbook = require('../gitbook');
var PLUGIN_PREFIX = 'gitbook-plugin-';
@@ -62,14 +64,35 @@ BookPlugin.prototype.load = function() {
return true;
})
- .then(function() {
- if (!that.isLoaded()) {
- throw new Error('Couldn\'t locate plugin "' + that.id + '", Run \'gitbook install\' to install plugins from registry.');
- }
- });
+ .then(that.validate);
this.log.info.log('Loading plugin "' + this.id + '" ...');
return this.log.info.promise('', promise);
};
+
+// Verify the definition of a plugin
+// Also verify that the plugin accepts the current gitbook version
+// This method throws erros if plugin is invalid
+BookPlugin.prototype.validate = function() {
+ var isValid = (
+ this.packageInfos &&
+ this.packageInfos.name &&
+ this.packageInfos.engines &&
+ this.packageInfos.engines.gitbook
+ );
+
+ if (!this.isLoaded()) {
+ throw new Error('Couldn\'t locate plugin "' + this.id + '", Run \'gitbook install\' to install plugins from registry.');
+ }
+
+ if (!isValid) {
+ throw new Error('Invalid plugin "' + this.id + '"');
+ }
+
+ if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) {
+ throw new Error('GitBook doesn\'t satisfy the requirements of this plugin: '+this.packageInfos.engines.gitbook);
+ }
+};
+
module.exports = BookPlugin;
From 702db46f20d2b613540901311708cf4e49d95690 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Tue, 16 Feb 2016 15:53:12 +0100
Subject: [PATCH 063/217] Add base for plugin linking/installation
---
lib/plugins/plugin.js | 13 ++-----------
lib/plugins/registry.js | 38 ++++++++++++++++++++++++++++++++++++++
2 files changed, 40 insertions(+), 11 deletions(-)
create mode 100644 lib/plugins/registry.js
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 05fa8ca04..7ce29e4d3 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -1,25 +1,16 @@
-var _ = require('lodash');
var path = require('path');
var resolve = require('resolve');
var Promise = require('../utils/promise');
var gitbook = require('../gitbook');
-
-var PLUGIN_PREFIX = 'gitbook-plugin-';
-
-// Return an absolute name for the plugin (the one on NPM)
-function npmId(name) {
- if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
- return [PLUGIN_PREFIX, name].join('');
-}
-
+var registry = require('./registry');
function BookPlugin(book, pluginId) {
this.book = book;
this.log = this.book.log;
this.id = pluginId;
- this.npmId = npmId(pluginId);
+ this.npmId = registry.npmId(pluginId);
this.baseDir;
this.packageInfos = undefined;
diff --git a/lib/plugins/registry.js b/lib/plugins/registry.js
new file mode 100644
index 000000000..6b1cddba4
--- /dev/null
+++ b/lib/plugins/registry.js
@@ -0,0 +1,38 @@
+var npm = require('npm');
+
+var PLUGIN_PREFIX = 'gitbook-plugin-';
+
+// Return an absolute name for the plugin (the one on NPM)
+function npmId(name) {
+ if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
+ return [PLUGIN_PREFIX, name].join('');
+}
+
+// Return a plugin ID 9the one on GitBook
+function pluginId(name) {
+ return name.replace(PLUGIN_PREFIX, '');
+}
+
+// Validate an NPM plugin ID
+function validateId(name) {
+ return name.indexOf(PLUGIN_PREFIX) === 0;
+}
+
+// Link a plugin for use in a specific book
+function linkPlugin(book, pluginPath) {
+ book.log('linking', pluginPath);
+}
+
+// Install a plugin in a book
+function installPlugin(book, pluginId) {
+ book.log('installing plugin', pluginId);
+}
+
+module.exports = {
+ npmId: npmId,
+ pluginId: pluginId,
+ validateId: validateId,
+
+ link: linkPlugin,
+ install: installPlugin
+};
From ca85c0ecf35eb2265b35ae480fbe34b12cf4bafe Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Tue, 16 Feb 2016 18:46:16 +0100
Subject: [PATCH 064/217] Add method to install plugin from npm
Add tests for it
---
lib/plugins/registry.js | 81 ++++++++++++++++++++++++++++++++++++++++-
test/all.js | 1 +
test/plugins.js | 37 +++++++++++++++++++
3 files changed, 117 insertions(+), 2 deletions(-)
create mode 100644 test/plugins.js
diff --git a/lib/plugins/registry.js b/lib/plugins/registry.js
index 6b1cddba4..3fa532ce8 100644
--- a/lib/plugins/registry.js
+++ b/lib/plugins/registry.js
@@ -1,4 +1,10 @@
var npm = require('npm');
+var npmi = require('npmi');
+var semver = require('semver');
+var _ = require('lodash');
+
+var Promise = require('../utils/promise');
+var gitbook = require('../gitbook');
var PLUGIN_PREFIX = 'gitbook-plugin-';
@@ -18,14 +24,84 @@ function validateId(name) {
return name.indexOf(PLUGIN_PREFIX) === 0;
}
+// Initialize NPM for operations
+var initNPM = _.memoize(function() {
+ return Promise.nfcall(npm.load, {
+ silent: true,
+ loglevel: 'silent'
+ });
+});
+
// Link a plugin for use in a specific book
function linkPlugin(book, pluginPath) {
book.log('linking', pluginPath);
}
+// Resolve the latest version for a plugin
+function resolveVersion(plugin) {
+ var npnName = npmId(plugin);
+
+ return initNPM()
+ .then(function() {
+ return Promise.nfcall(npm.commands.view, [npnName+'@*', 'engines'], true);
+ })
+ .then(function(versions) {
+ return _.chain(versions)
+ .pairs()
+ .map(function(v) {
+ return {
+ version: v[0],
+ gitbook: (v[1].engines || {}).gitbook
+ };
+ })
+ .filter(function(v) {
+ return v.gitbook && gitbook.satisfies(v.gitbook);
+ })
+ .sort(function(v1, v2) {
+ return semver.lt(v1.version, v2.version)? 1 : -1;
+ })
+ .pluck('version')
+ .first()
+ .value();
+ });
+}
+
+
// Install a plugin in a book
-function installPlugin(book, pluginId) {
- book.log('installing plugin', pluginId);
+function installPlugin(book, plugin, version) {
+ book.log.info.ln('installing plugin', plugin);
+
+ var npnName = npmId(plugin);
+
+ return Promise()
+ .then(function() {
+ if (version) return version;
+
+ book.log.info.ln('No version specified, resolve plugin "' + plugin + '"');
+ return resolveVersion(plugin);
+ })
+
+ // Install the plugin with the resolved version
+ .then(function(version) {
+ if (!version) {
+ throw new Error('Found no satisfactory version for plugin "' + plugin + '"');
+ }
+
+ book.log.info.ln('install plugin' + plugin +'" from npm ('+npnName+') with version', version);
+ return Promise.nfcall(npmi, {
+ 'name': npnName,
+ 'version': version,
+ 'path': book.root,
+ 'npmLoad': {
+ 'loglevel': 'silent',
+ 'loaded': true,
+ 'prefix': book.root
+ }
+ });
+ })
+ .then(function() {
+ book.log.info.ok('plugin "' + plugin.name + '" installed with success');
+ });
}
module.exports = {
@@ -33,6 +109,7 @@ module.exports = {
pluginId: pluginId,
validateId: validateId,
+ resolve: resolveVersion,
link: linkPlugin,
install: installPlugin
};
diff --git a/test/all.js b/test/all.js
index fed3f61a4..c7c2bf79f 100644
--- a/test/all.js
+++ b/test/all.js
@@ -10,6 +10,7 @@ require('./parse');
require('./git');
require('./template');
require('./conrefs');
+require('./plugins');
// Page and HTML generation
require('./page');
diff --git a/test/plugins.js b/test/plugins.js
new file mode 100644
index 000000000..123a194fc
--- /dev/null
+++ b/test/plugins.js
@@ -0,0 +1,37 @@
+var mock = require('./mock');
+var registry = require('../lib/plugins/registry');
+
+describe('Plugins', function() {
+ var book;
+
+ before(function() {
+ return mock.setupBook({})
+ .then(function(_book) {
+ book = _book;
+ });
+ });
+
+ describe('Resolve Version', function() {
+
+ it('should resolve a plugin version', function() {
+ return registry.resolve('ga')
+ .should.be.fulfilled();
+ });
+
+ });
+
+ describe('Installation', function() {
+
+ it('should install a plugin from NPM without a specific version', function() {
+ return registry.install(book, 'ga')
+ .should.be.fulfilled();
+ });
+
+ it('should install a plugin from NPM with a specific version', function() {
+ return registry.install(book, 'ga', '1.0.0')
+ .should.be.fulfilled();
+ });
+
+ });
+});
+
From db3d21db49a7260df03ae987b58c495178193dde Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Tue, 16 Feb 2016 23:30:08 +0100
Subject: [PATCH 065/217] Start commands for plugins installation
---
lib/index.js | 36 +++++++++++++++++++++++++++++++++++-
lib/output/base.js | 4 ++++
lib/output/conrefs.js | 7 ++-----
lib/output/json.js | 5 ++++-
lib/output/website/index.js | 5 ++++-
lib/plugins/index.js | 8 +++++++-
6 files changed, 56 insertions(+), 9 deletions(-)
diff --git a/lib/index.js b/lib/index.js
index 59346f2de..e0d43eb24 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,6 +1,40 @@
var Book = require('./book');
var Output = require('./output');
+var NodeFS = require('./fs/node');
+
+// Setup a Book for the arguments
+function setupBook(args) {
+ var input = args[0] || process.cwd();
+ return new Book({
+ fs: new NodeFS(),
+ root: input
+ });
+}
+
+// Setup an Output for the arguments
+function setupOutput(Out, args) {
+ return new Out(setupBook(args));
+}
module.exports = {
- Book: Book
+ Book: Book,
+ commands: [
+ {
+ name: 'install [book]',
+ description: 'install all plugins dependencies',
+ exec: function(args) {
+ var book = setupBook(args);
+
+ return book.config.load()
+ .then(function() {
+ return book.plugins.install();
+ })
+ .then(function(){
+ console.log('');
+ console.log(color.green('Done, without error'));
+ });
+ }
+ },
+
+ ]
};
diff --git a/lib/output/base.js b/lib/output/base.js
index fede3da66..ce0a9c63e 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -150,4 +150,8 @@ Output.prototype.finish = function() {
};
+Output.createMixin = function(def) {
+
+};
+
module.exports = Output;
diff --git a/lib/output/conrefs.js b/lib/output/conrefs.js
index 98230d99f..c91885ee3 100644
--- a/lib/output/conrefs.js
+++ b/lib/output/conrefs.js
@@ -1,4 +1,3 @@
-var util = require('util');
var path = require('path');
var Output = require('./base');
@@ -10,11 +9,9 @@ var pathUtil = require('../utils/path');
Middleware for output to resolve git conrefs
*/
-function ConrefsLoader() {
- Output.apply(this, arguments);
+var ConrefsLoader = Output.createMixin(function() {
this.git = new Git();
-}
-util.inherits(ConrefsLoader, Output);
+});
// Read a template by its source URL
ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
diff --git a/lib/output/json.js b/lib/output/json.js
index a77b45a46..f1460235c 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,10 +1,13 @@
var util = require('util');
+var FolderOutput = require('./folder');
var ConrefsLoader = require('./conrefs');
var gitbook = require('../gitbook');
function JSONOutput() {
- ConrefsLoader.apply(this, arguments);
+ FolderOutput.apply(this, arguments);
+ ConrefsLoader.apply(this);
}
+util.inherits(JSONOutput, FolderOutput);
util.inherits(JSONOutput, ConrefsLoader);
JSONOutput.prototype.name = 'json';
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index 7322a578d..fc9bc6b0f 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -1,11 +1,14 @@
var util = require('util');
+var FolderOutput = require('../folder');
var ConrefsLoader = require('../conrefs');
var Theme = require('./theme');
function WebsiteOutput() {
- ConrefsLoader.apply(this, arguments);
+ FolderOutput.apply(this, arguments);
+ ConrefsLoader.apply(this);
}
+util.inherits(WebsiteOutput, FolderOutput);
util.inherits(WebsiteOutput, ConrefsLoader);
WebsiteOutput.prototype.name = 'website';
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index ac595ac69..14a323a96 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -2,7 +2,7 @@ var _ = require('lodash');
var Promise = require('../utils/promise');
var BookPlugin = require('./plugin');
-
+var registry = require('./registry');
/*
PluginsManager is an interface to work with multiple plugins at once:
@@ -13,6 +13,7 @@ PluginsManager is an interface to work with multiple plugins at once:
function PluginsManager(output) {
this.output = output;
this.book = output.book;
+ this.log = this.book.log;
this.plugins = [];
}
@@ -62,4 +63,9 @@ PluginsManager.prototype._setup = function(plugin) {
};
+// Install all plugins for the book
+PluginsManager.prototype.install = function() {
+
+};
+
module.exports = PluginsManager;
From 8141bcb3b63f16c27f8cd6c5e19aed4b5ef6d019 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 10:11:33 +0100
Subject: [PATCH 066/217] Add command 'parse' for gitbook-cli
---
lib/cli/helper.js | 50 +++++++++++++++++++++++++++++++++++++++++++++
lib/cli/index.js | 44 +++++++++++++++++++++++++++++++++++++++
lib/config/index.js | 2 +-
lib/index.js | 37 ++-------------------------------
lib/output/base.js | 2 +-
5 files changed, 98 insertions(+), 37 deletions(-)
create mode 100644 lib/cli/helper.js
create mode 100644 lib/cli/index.js
diff --git a/lib/cli/helper.js b/lib/cli/helper.js
new file mode 100644
index 000000000..67bc5021b
--- /dev/null
+++ b/lib/cli/helper.js
@@ -0,0 +1,50 @@
+var _ = require('lodash');
+
+var Book = require('../book');
+var NodeFS = require('../fs/node');
+var Logger = require('../utils/logger');
+
+var LOG_OPTION = {
+ name: 'log',
+ description: 'Minimum log level to display',
+ values: _.chain(Logger.LEVELS)
+ .keys()
+ .map(function(s) {
+ return s.toLowerCase();
+ })
+ .value(),
+ defaults: 'info'
+};
+
+// Commands which is processing a book
+// the root of the book is the first argument (or current directory)
+function bookCmd(fn) {
+ return function(args, kwargs) {
+
+ var input = args[0] || process.cwd();
+ var book = new Book({
+ fs: new NodeFS(),
+ root: input,
+
+ logLevel: kwargs.log
+ });
+
+ return fn(book, args.slice(1));
+ };
+}
+
+// Commands which is working on a Output instance
+function outputCmd(Out, fn) {
+ return bookCmd(function(book, args) {
+ return fn(new Out(book), args);
+ });
+}
+
+module.exports = {
+ bookCmd: bookCmd,
+ outputCmd: outputCmd,
+
+ options: {
+ log: LOG_OPTION
+ }
+};
diff --git a/lib/cli/index.js b/lib/cli/index.js
new file mode 100644
index 000000000..d67b78a84
--- /dev/null
+++ b/lib/cli/index.js
@@ -0,0 +1,44 @@
+var Output = require('../output/base');
+var helper = require('./helper');
+
+module.exports = {
+ commands: [
+
+ {
+ name: 'parse [book]',
+ description: 'parse and returns debug information for a book',
+ options: [
+ helper.options.log
+ ],
+ exec: helper.bookCmd(function(book) {
+ return book.parse()
+ .then(function() {
+ book.log.info.ln('');
+
+ if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path);
+
+ if (book.isMultilingual()) {
+ book.log.info.ln('Multilingual book detected:', book.langs.path);
+ } else {
+ book.log.info.ln('Readme:', book.readme.path);
+ book.log.info.ln('Summary:', book.summary.path);
+ if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path);
+ }
+ });
+ })
+ },
+
+ {
+ name: 'install [book]',
+ description: 'install all plugins dependencies',
+ options: [
+ helper.options.log
+ ],
+ exec: helper.outputCmd(Output, function(output, args) {
+
+ })
+ }
+
+
+ ]
+};
diff --git a/lib/config/index.js b/lib/config/index.js
index d83dae479..801dca118 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -19,7 +19,7 @@ function Config(book, baseConfig) {
this.book = book;
this.fs = book.fs;
this.log = book.log;
- this.path = null;
+ this.path = '';
this.replace(baseConfig || {});
}
diff --git a/lib/index.js b/lib/index.js
index e0d43eb24..fdad6ee2c 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,40 +1,7 @@
var Book = require('./book');
-var Output = require('./output');
-var NodeFS = require('./fs/node');
-
-// Setup a Book for the arguments
-function setupBook(args) {
- var input = args[0] || process.cwd();
- return new Book({
- fs: new NodeFS(),
- root: input
- });
-}
-
-// Setup an Output for the arguments
-function setupOutput(Out, args) {
- return new Out(setupBook(args));
-}
+var cli = require('./cli');
module.exports = {
Book: Book,
- commands: [
- {
- name: 'install [book]',
- description: 'install all plugins dependencies',
- exec: function(args) {
- var book = setupBook(args);
-
- return book.config.load()
- .then(function() {
- return book.plugins.install();
- })
- .then(function(){
- console.log('');
- console.log(color.green('Done, without error'));
- });
- }
- },
-
- ]
+ commands: cli.commands
};
diff --git a/lib/output/base.js b/lib/output/base.js
index ce0a9c63e..193f6369c 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -20,7 +20,7 @@ function Output(book) {
this.log = this.book.log;
// Create plugins manager
- this.plugins = new PluginsManager(book);
+ this.plugins = new PluginsManager(this);
// Create template engine
this.template = new TemplateEngine(this);
From f54685e2168bd54b06664b55ffb14334fef4cf80 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 11:25:57 +0100
Subject: [PATCH 067/217] Change format of conrefs and folder mixins
---
lib/output/assets-inliner.js | 199 ++++++++++++++++++-----------------
lib/output/conrefs.js | 100 ++++++++++--------
lib/output/folder.js | 192 ++++++++++++++++-----------------
lib/output/json.js | 12 +--
lib/output/website/index.js | 11 +-
test/assets-inliner.js | 2 +-
test/conrefs.js | 2 +-
7 files changed, 259 insertions(+), 259 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index 97335050d..b5d076eac 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -2,7 +2,7 @@ var util = require('util');
var path = require('path');
var crc = require('crc');
-var FolderOutput = require('./folder');
+var FolderOutput = require('./folder')();
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var imagesUtil = require('../utils/images');
@@ -11,123 +11,126 @@ var location = require('../utils/location');
var DEFAULT_ASSETS_FOLDER = 'assets';
/*
-Utility mixin to inline all the assets in a book:
+Mixin to inline all the assets in a book:
- Outline tags
- Download remote images
- Convert .svg images as png
*/
-function AssetsInliner() {
- FolderOutput.apply(this, arguments);
+module.exports = function assetsInliner(Base) {
+ Base = Base || FolderOutput;
- // Map of svg already converted
- this.svgs = {};
- this.inlineSvgs = {};
+ function AssetsInliner() {
+ Base.apply(this, arguments);
- // Map of images already downloaded
- this.downloaded = {};
-}
-util.inherits(AssetsInliner, FolderOutput);
+ // Map of svg already converted
+ this.svgs = {};
+ this.inlineSvgs = {};
-// Output a SVG buffer as a file
-AssetsInliner.prototype.onOutputSVG = function(page, svg) {
- this.log.debug.ln('output svg from', page.path);
+ // Map of images already downloaded
+ this.downloaded = {};
+ }
+ util.inherits(AssetsInliner, Base);
- // Convert svg buffer to a png file
- return this.convertSVGBuffer(svg)
+ // Output a SVG buffer as a file
+ AssetsInliner.prototype.onOutputSVG = function(page, svg) {
+ this.log.debug.ln('output svg from', page.path);
+
+ // Convert svg buffer to a png file
+ return this.convertSVGBuffer(svg)
+
+ // Return relative path from the page
+ .then(function(filename) {
+ return page.relative('/' + filename);
+ });
+ };
+
+
+ // Output an image as a file
+ AssetsInliner.prototype.onOutputImage = function(page, src) {
+ var that = this;
+
+ return Promise()
+
+ // Download file if external
+ .then(function() {
+ if (!location.isExternal(src)) return;
+
+ return that.downloadAsset(src)
+ .then(function(_asset) {
+ src = '/' + _asset;
+ });
+
+ })
+ .then(function() {
+ if (path.extname(src).toLowerCase() != '.svg') {
+ return src;
+ }
+
+ // Convert SVG to PNG
+ return that.convertSVGFile(that.resolveForPage(page, src));
+ })
// Return relative path from the page
.then(function(filename) {
return page.relative('/' + filename);
});
-};
+ };
+ // Download an asset if not already download; returns the output file
+ AssetsInliner.prototype.downloadAsset = function(src) {
+ if (this.downloaded[src]) return Promise(this.downloaded[src]);
-// Output an image as a file
-AssetsInliner.prototype.onOutputImage = function(page, src) {
- var that = this;
+ var that = this;
+ var ext = path.extname(src);
+ var hash = crc.crc32(src).toString(16);
- return Promise()
+ // Create new file
+ return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + ext)
+ .then(function(filename) {
+ that.downloaded[src] = filename;
- // Download file if external
- .then(function() {
- if (!location.isExternal(src)) return;
-
- return that.downloadAsset(src)
- .then(function(_asset) {
- src = '/' + _asset;
+ that.log.debug.ln('downloading asset', src);
+ return fs.download(src, that.resolve(filename))
+ .thenResolve(filename);
});
+ };
- })
- .then(function() {
- if (path.extname(src).toLowerCase() != '.svg') {
- return src;
- }
+ // Convert a .svg into an .png
+ // Return the output filename for the .png
+ AssetsInliner.prototype.convertSVGFile = function(src) {
+ if (this.svgs[src]) return Promise(this.svgs[src]);
- // Convert SVG to PNG
- return that.convertSVGFile(that.resolveForPage(page, src));
- })
+ var that = this;
+ var hash = crc.crc32(src).toString(16);
- // Return relative path from the page
- .then(function(filename) {
- return page.relative('/' + filename);
- });
+ // Create new file
+ return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
+ .then(function(filename) {
+ that.svgs[src] = filename;
+
+ return imagesUtil.convertSVGToPNG(src, that.resolve(filename))
+ .thenResolve(filename);
+ });
+ };
+
+ // Convert an inline svg into an .png
+ // Return the output filename for the .png
+ AssetsInliner.prototype.convertSVGBuffer = function(buf) {
+ var that = this;
+ var hash = crc.crc32(buf).toString(16);
+
+ // Already converted?
+ if (this.inlineSvgs[hash]) return Promise(this.inlineSvgs[hash]);
+
+ return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
+ .then(function(filename) {
+ that.inlineSvgs[hash] = filename;
+
+ return imagesUtil.convertSVGBufferToPNG(buf, that.resolve(filename))
+ .thenResolve(filename);
+ });
+ };
+
+ return AssetsInliner;
};
-
-// Download an asset if not already download; returns the output file
-AssetsInliner.prototype.downloadAsset = function(src) {
- if (this.downloaded[src]) return Promise(this.downloaded[src]);
-
- var that = this;
- var ext = path.extname(src);
- var hash = crc.crc32(src).toString(16);
-
- // Create new file
- return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + ext)
- .then(function(filename) {
- that.downloaded[src] = filename;
-
- that.log.debug.ln('downloading asset', src);
- return fs.download(src, that.resolve(filename))
- .thenResolve(filename);
- });
-};
-
-// Convert a .svg into an .png
-// Return the output filename for the .png
-AssetsInliner.prototype.convertSVGFile = function(src) {
- if (this.svgs[src]) return Promise(this.svgs[src]);
-
- var that = this;
- var hash = crc.crc32(src).toString(16);
-
- // Create new file
- return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
- .then(function(filename) {
- that.svgs[src] = filename;
-
- return imagesUtil.convertSVGToPNG(src, that.resolve(filename))
- .thenResolve(filename);
- });
-};
-
-// Convert an inline svg into an .png
-// Return the output filename for the .png
-AssetsInliner.prototype.convertSVGBuffer = function(buf) {
- var that = this;
- var hash = crc.crc32(buf).toString(16);
-
- // Already converted?
- if (this.inlineSvgs[hash]) return Promise(this.inlineSvgs[hash]);
-
- return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
- .then(function(filename) {
- that.inlineSvgs[hash] = filename;
-
- return imagesUtil.convertSVGBufferToPNG(buf, that.resolve(filename))
- .thenResolve(filename);
- });
-};
-
-
-module.exports = AssetsInliner;
diff --git a/lib/output/conrefs.js b/lib/output/conrefs.js
index c91885ee3..f1cd1233b 100644
--- a/lib/output/conrefs.js
+++ b/lib/output/conrefs.js
@@ -1,60 +1,68 @@
var path = require('path');
+var util = require('util');
-var Output = require('./base');
+var FolderOutput = require('./folder')();
var Git = require('../utils/git');
var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
/*
-Middleware for output to resolve git conrefs
+Mixin for output to resolve git conrefs
*/
-var ConrefsLoader = Output.createMixin(function() {
- this.git = new Git();
-});
+module.exports = function conrefsLoader(Base) {
+ Base = Base || FolderOutput;
-// Read a template by its source URL
-ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
- var that = this;
+ function ConrefsLoader() {
+ Base.apply(this, arguments);
- return this.git.resolve(sourceURL)
- .then(function(filepath) {
- // Is local file
- if (!filepath) {
- filepath = that.book.resolve(sourceURL);
- } else {
- that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
+ this.git = new Git();
+ }
+ util.inherits(ConrefsLoader, Base);
+
+ // Read a template by its source URL
+ ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
+ var that = this;
+
+ return this.git.resolve(sourceURL)
+ .then(function(filepath) {
+ // Is local file
+ if (!filepath) {
+ filepath = that.book.resolve(sourceURL);
+ } else {
+ that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
+ }
+
+ // Read file from absolute path
+ return fs.readFile(filepath)
+ .then(function(source) {
+ return {
+ src: source.toString('utf8'),
+ path: filepath
+ };
+ });
+ });
+ };
+
+ // Generate a source URL for a template
+ ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
+ // If origin is in the book, we enforce result file to be in the book
+ if (this.book.isInBook(from)) {
+ return this.book.resolve(
+ this.book.relative(path.dirname(from)),
+ to
+ );
}
- // Read file from absolute path
- return fs.readFile(filepath)
- .then(function(source) {
- return {
- src: source.toString('utf8'),
- path: filepath
- };
- });
- });
+ // If origin is in a git repository, we resolve file in the git repository
+ var gitRoot = this.git.resolveRoot(from);
+ if (gitRoot) {
+ return pathUtil.resolveInRoot(gitRoot, to);
+ }
+
+ // If origin is not in the book (include from a git content ref)
+ return path.resolve(path.dirname(from), to);
+ };
+
+ return ConrefsLoader;
};
-
-// Generate a source URL for a template
-ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
- // If origin is in the book, we enforce result file to be in the book
- if (this.book.isInBook(from)) {
- return this.book.resolve(
- this.book.relative(path.dirname(from)),
- to
- );
- }
-
- // If origin is in a git repository, we resolve file in the git repository
- var gitRoot = this.git.resolveRoot(from);
- if (gitRoot) {
- return pathUtil.resolveInRoot(gitRoot, to);
- }
-
- // If origin is not in the book (include from a git content ref)
- return path.resolve(path.dirname(from), to);
-};
-
-module.exports = ConrefsLoader;
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 90a33511b..9964ecd91 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -12,104 +12,106 @@ This output requires the native fs module to output
book as a directory (mapping assets and pages)
*/
-function FolderOutput() {
- Output.apply(this, arguments);
-}
-util.inherits(FolderOutput, Output);
+module.exports = function folderOutput(Base) {
+ Base = Base || Output;
-// Copy an asset file (non-parsable), ex: images, etc
-FolderOutput.prototype.onAsset = function(filename) {
- return this.copyFile(
- this.book.resolve(filename),
- filename
- );
-};
-
-// Prepare the generation by creating the output folder
-FolderOutput.prototype.prepare = function() {
- return fs.mkdirp(this.root());
-};
-
-
-// ----- Utility methods -----
-
-// Return path to the root folder
-FolderOutput.prototype.root = function() {
- return path.resolve(process.cwd(), this.book.config.get('output'));
-};
-
-// Resolve a file in the output directory
-FolderOutput.prototype.resolve = function(filename) {
- return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
-};
-
-// Resolve a file path from a page (in the output folder)
-FolderOutput.prototype.resolveForPage = function(page, filename) {
- var abs = page.resolveLocal(filename);
- return this.resolve(abs);
-};
-
-
-// Copy a file to the output
-FolderOutput.prototype.copyFile = function(from, to) {
- var that = this;
-
- return Promise()
- .then(function() {
- to = that.resolve(to);
-
- return fs.copy(from, to);
- });
-};
-
-// Write a file/buffer to the output folder
-FolderOutput.prototype.writeFile = function(filename, buf) {
- var that = this;
-
- return Promise()
- .then(function() {
- filename = that.resolve(filename);
- var folder = path.dirname(filename);
-
- // Ensure fodler exists
- return fs.mkdirp(folder);
- })
-
- // Write the file
- .then(function() {
- return fs.writeFile(filename, buf);
- });
-};
-
-// Return true if a file exists in the output folder
-FolderOutput.prototype.hasFile = function(filename) {
- var that = this;
-
- return Promise()
- .then(function() {
- return fs.exists(that.resolve(filename));
- });
-};
-
-
-// Create a new unique file
-// Returns its filename
-FolderOutput.prototype.createNewFile = function(base, filename) {
- var that = this;
-
- if (!filename) {
- filename = path.basename(filename);
- base = path.dirname(base);
+ function FolderOutput() {
+ Base.apply(this, arguments);
}
+ util.inherits(FolderOutput, Base);
- return fs.uniqueFilename(this.resolve(base), filename)
- .then(function(out) {
- out = path.join(base, out);
+ // Copy an asset file (non-parsable), ex: images, etc
+ FolderOutput.prototype.onAsset = function(filename) {
+ return this.copyFile(
+ this.book.resolve(filename),
+ filename
+ );
+ };
- return fs.ensure(that.resolve(out))
- .thenResolve(out);
+ // Prepare the generation by creating the output folder
+ FolderOutput.prototype.prepare = function() {
+ return fs.mkdirp(this.root());
+ };
+
+
+ // ----- Utility methods -----
+
+ // Return path to the root folder
+ FolderOutput.prototype.root = function() {
+ return path.resolve(process.cwd(), this.book.config.get('output'));
+ };
+
+ // Resolve a file in the output directory
+ FolderOutput.prototype.resolve = function(filename) {
+ return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
+ };
+
+ // Resolve a file path from a page (in the output folder)
+ FolderOutput.prototype.resolveForPage = function(page, filename) {
+ var abs = page.resolveLocal(filename);
+ return this.resolve(abs);
+ };
+
+
+ // Copy a file to the output
+ FolderOutput.prototype.copyFile = function(from, to) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ to = that.resolve(to);
+
+ return fs.copy(from, to);
});
+ };
+
+ // Write a file/buffer to the output folder
+ FolderOutput.prototype.writeFile = function(filename, buf) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ filename = that.resolve(filename);
+ var folder = path.dirname(filename);
+
+ // Ensure fodler exists
+ return fs.mkdirp(folder);
+ })
+
+ // Write the file
+ .then(function() {
+ return fs.writeFile(filename, buf);
+ });
+ };
+
+ // Return true if a file exists in the output folder
+ FolderOutput.prototype.hasFile = function(filename) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ return fs.exists(that.resolve(filename));
+ });
+ };
+
+ // Create a new unique file
+ // Returns its filename
+ FolderOutput.prototype.createNewFile = function(base, filename) {
+ var that = this;
+
+ if (!filename) {
+ filename = path.basename(filename);
+ base = path.dirname(base);
+ }
+
+ return fs.uniqueFilename(this.resolve(base), filename)
+ .then(function(out) {
+ out = path.join(base, out);
+
+ return fs.ensure(that.resolve(out))
+ .thenResolve(out);
+ });
+ };
+
+ return FolderOutput;
};
-
-
-module.exports = FolderOutput;
diff --git a/lib/output/json.js b/lib/output/json.js
index f1460235c..913bc2b0b 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,14 +1,8 @@
-var util = require('util');
-var FolderOutput = require('./folder');
-var ConrefsLoader = require('./conrefs');
var gitbook = require('../gitbook');
+var conrefsLoader = require('./conrefs');
-function JSONOutput() {
- FolderOutput.apply(this, arguments);
- ConrefsLoader.apply(this);
-}
-util.inherits(JSONOutput, FolderOutput);
-util.inherits(JSONOutput, ConrefsLoader);
+
+var JSONOutput = conrefsLoader();
JSONOutput.prototype.name = 'json';
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
index fc9bc6b0f..6166ccc46 100644
--- a/lib/output/website/index.js
+++ b/lib/output/website/index.js
@@ -1,15 +1,8 @@
-var util = require('util');
-var FolderOutput = require('../folder');
-var ConrefsLoader = require('../conrefs');
+var conrefsLoader = require('../conrefs');
var Theme = require('./theme');
-function WebsiteOutput() {
- FolderOutput.apply(this, arguments);
- ConrefsLoader.apply(this);
-}
-util.inherits(WebsiteOutput, FolderOutput);
-util.inherits(WebsiteOutput, ConrefsLoader);
+var WebsiteOutput = conrefsLoader();
WebsiteOutput.prototype.name = 'website';
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index 1f00b5dea..498d3cbc4 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -2,7 +2,7 @@ var cheerio = require('cheerio');
var path = require('path');
var mock = require('./mock');
-var AssetsInliner = require('../lib/output/assets-inliner');
+var AssetsInliner = require('../lib/output/assets-inliner')();
describe('Assets Inliner Output', function() {
var output;
diff --git a/test/conrefs.js b/test/conrefs.js
index e75055cdb..65007095f 100644
--- a/test/conrefs.js
+++ b/test/conrefs.js
@@ -1,5 +1,5 @@
var mock = require('./mock');
-var ConrefsLoader = require('../lib/output/conrefs');
+var ConrefsLoader = require('../lib/output/conrefs')();
describe('Conrefs Loader', function() {
From c89c253f294df6820eeea6f50ec98dcd672ec8d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 11:26:36 +0100
Subject: [PATCH 068/217] Adapt ebook output
---
lib/output/ebook.js | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index b4cd55088..010f3d741 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -1,14 +1,7 @@
-var util = require('util');
-
var WebsiteOutput = require('./website');
-var AssetsInliner = require('./assets-inliner');
+var assetsInliner = require('./assets-inliner');
-function EbookOutput() {
- WebsiteOutput.apply(this, arguments);
- AssetsInliner.call(this);
-}
-util.inherits(EbookOutput, AssetsInliner);
-util.inherits(EbookOutput, WebsiteOutput);
+var EbookOutput = assetsInliner(WebsiteOutput);
EbookOutput.prototype.name = 'ebook';
From 7c575e1b43d75a22bad035a3ce62110572dff1c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 11:52:31 +0100
Subject: [PATCH 069/217] Fix loading of plugins
---
lib/cli/index.js | 6 ++--
lib/config/index.js | 11 ++++---
lib/config/plugins.js | 71 +++++++++++++++++++++++++++++++++++++++++++
lib/output/base.js | 2 +-
lib/plugins/index.js | 16 +++++++---
lib/plugins/plugin.js | 16 ++++++----
test/all.js | 2 +-
test/plugins.js | 12 ++++++--
8 files changed, 113 insertions(+), 23 deletions(-)
create mode 100644 lib/config/plugins.js
diff --git a/lib/cli/index.js b/lib/cli/index.js
index d67b78a84..4bb926bc3 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -1,4 +1,5 @@
var Output = require('../output/base');
+var PluginsManager = require('../plugins');
var helper = require('./helper');
module.exports = {
@@ -34,8 +35,9 @@ module.exports = {
options: [
helper.options.log
],
- exec: helper.outputCmd(Output, function(output, args) {
-
+ exec: helper.bookCmd(function(book, args) {
+ var plugins = new PluginsManager(book);
+ return plugins.install();
})
}
diff --git a/lib/config/index.js b/lib/config/index.js
index 801dca118..ede45ba79 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -2,8 +2,9 @@ var _ = require('lodash');
var semver = require('semver');
var gitbook = require('../gitbook');
-var configDefault = require('./default');
var Promise = require('../utils/promise');
+var configDefault = require('./default');
+var plugins = require('./plugins');
// Config files to tested (sorted)
var CONFIG_FILES = [
@@ -56,10 +57,10 @@ Config.prototype.load = function() {
}
that.options.output = that.options.output || that.book.resolve('_book');
- //that.options.plugins = normalizePluginsList(that.options.plugins);
- //that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false);
- //that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
- //that.options.plugins = _.uniq(that.options.plugins, 'name');
+ that.options.plugins = plugins.toList(that.options.plugins);
+ that.options.defaultsPlugins = plugins.toList(that.options.defaultsPlugins || '', false);
+ that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
+ that.options.plugins = _.uniq(that.options.plugins, 'name');
// Default value for text direction (from language)
/*if (!that.options.direction) {
diff --git a/lib/config/plugins.js b/lib/config/plugins.js
new file mode 100644
index 000000000..43eafdfe6
--- /dev/null
+++ b/lib/config/plugins.js
@@ -0,0 +1,71 @@
+var _ = require('lodash');
+
+// Default plugins added to each books
+var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings'];
+
+// Return true if a plugin is a default plugin
+function isDefaultPlugin(name, version) {
+ return _.contains(DEFAULT_PLUGINS, name);
+}
+
+// Normalize a list of plugins to use
+function normalizePluginsList(plugins, addDefaults) {
+ // Normalize list to an array
+ plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []);
+
+ // Remove empty parts
+ plugins = _.compact(plugins);
+
+ // Divide as {name, version} to handle format like 'myplugin@1.0.0'
+ plugins = _.map(plugins, function(plugin) {
+ if (plugin.name) return plugin;
+
+ var parts = plugin.split('@');
+ var name = parts[0];
+ var version = parts[1];
+ return {
+ 'name': name,
+ 'version': version, // optional
+ 'isDefault': isDefaultPlugin(name, version)
+ };
+ });
+
+ // List plugins to remove
+ var toremove = _.chain(plugins)
+ .filter(function(plugin) {
+ return plugin.name.length > 0 && plugin.name[0] == '-';
+ })
+ .map(function(plugin) {
+ return plugin.name.slice(1);
+ })
+ .value();
+
+ // Merge with defaults
+ if (addDefaults !== false) {
+ _.each(DEFAULT_PLUGINS, function(plugin) {
+ if (_.find(plugins, { name: plugin })) {
+ return;
+ }
+
+ plugins.push({
+ 'name': plugin,
+ 'isDefault': true
+ });
+ });
+ }
+
+ // Remove plugin that start with '-'
+ plugins = _.filter(plugins, function(plugin) {
+ return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-');
+ });
+
+ // Remove duplicates
+ plugins = _.uniq(plugins, 'name');
+
+ return plugins;
+}
+
+module.exports = {
+ toList: normalizePluginsList
+};
+
diff --git a/lib/output/base.js b/lib/output/base.js
index 193f6369c..2b9cdc7a1 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -20,7 +20,7 @@ function Output(book) {
this.log = this.book.log;
// Create plugins manager
- this.plugins = new PluginsManager(this);
+ this.plugins = new PluginsManager(this.book);
// Create template engine
this.template = new TemplateEngine(this);
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 14a323a96..f26c63d32 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -10,13 +10,19 @@ PluginsManager is an interface to work with multiple plugins at once:
- Call hooks for all plugins, etc
*/
-function PluginsManager(output) {
- this.output = output;
- this.book = output.book;
+function PluginsManager(book) {
+ this.book = book;
this.log = this.book.log;
this.plugins = [];
+
+ _.bindAll(this);
}
+// Return count of plugins loaded
+PluginsManager.prototype.count = function() {
+ return _.size(this.plugins);
+};
+
// Returns a plugin by its name
PluginsManager.prototype.get = function(name) {
return _.find(this.plugins, {
@@ -60,12 +66,12 @@ PluginsManager.prototype.load = function(name) {
// Setup a plugin
// Register its filter, blocks, etc
PluginsManager.prototype._setup = function(plugin) {
-
+ this.plugins.push(plugin);
};
// Install all plugins for the book
PluginsManager.prototype.install = function() {
-
+ this.log.info.ln('installing 0 plugins');
};
module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 7ce29e4d3..4ed619c9c 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -1,3 +1,4 @@
+var _ = require('lodash');
var path = require('path');
var resolve = require('resolve');
@@ -15,6 +16,8 @@ function BookPlugin(book, pluginId) {
this.packageInfos = undefined;
this.content = undefined;
+
+ _.bindAll(this);
}
// Return true if plugin has been loaded correctly
@@ -31,18 +34,19 @@ BookPlugin.prototype.load = function() {
}
// Try loading plugins from different location
- var promise = Promise.some([
+ var p = Promise.some([
this.book.resolve('node_modules'),
__dirname
], function(baseDir) {
// Locate plugin and load pacjage.json
try {
- var res = resolve.sync(name + '/package.json', { basedir: baseDir });
+ var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });
that.baseDir = path.dirname(res);
that.packageInfos = require(res);
} catch (err) {
- if (err.code != 'MODULE_NOT_FOUND') throw err;
+ // Wait on https://github.com/substack/node-resolve/pull/81 to be merged
+ if (err.message.indexOf('Cannot find module') < 0) throw err;
that.packageInfos = undefined;
that.content = undefined;
@@ -51,14 +55,14 @@ BookPlugin.prototype.load = function() {
}
// Load plugin JS content
- that.content = require(resolve.sync(name, { basedir: baseDir }));
+ that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
return true;
})
.then(that.validate);
- this.log.info.log('Loading plugin "' + this.id + '" ...');
- return this.log.info.promise('', promise);
+ this.log.info('Loading plugin "' + this.id + '" ...');
+ return this.log.info.promise(p);
};
diff --git a/test/all.js b/test/all.js
index c7c2bf79f..df367b24c 100644
--- a/test/all.js
+++ b/test/all.js
@@ -8,9 +8,9 @@ require('./langs');
require('./parse');
require('./git');
+require('./plugins');
require('./template');
require('./conrefs');
-require('./plugins');
// Page and HTML generation
require('./page');
diff --git a/test/plugins.js b/test/plugins.js
index 123a194fc..81c099e13 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -1,5 +1,6 @@
var mock = require('./mock');
var registry = require('../lib/plugins/registry');
+var Output = require('../lib/output/base');
describe('Plugins', function() {
var book;
@@ -12,16 +13,13 @@ describe('Plugins', function() {
});
describe('Resolve Version', function() {
-
it('should resolve a plugin version', function() {
return registry.resolve('ga')
.should.be.fulfilled();
});
-
});
describe('Installation', function() {
-
it('should install a plugin from NPM without a specific version', function() {
return registry.install(book, 'ga')
.should.be.fulfilled();
@@ -31,7 +29,15 @@ describe('Plugins', function() {
return registry.install(book, 'ga', '1.0.0')
.should.be.fulfilled();
});
+ });
+ describe('Loading', function() {
+ it('should load default plugins', function() {
+ return mock.outputDefaultBook(Output)
+ .then(function(output) {
+ output.plugins.count().should.be.greaterThan(0);
+ });
+ });
});
});
From e2c21051aa6a42f13208297d08688b99d45ddb27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 12:02:58 +0100
Subject: [PATCH 070/217] Add method install to plugins manager
---
lib/output/base.js | 6 +-----
lib/plugins/index.js | 25 ++++++++++++++++++++++++-
2 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 2b9cdc7a1..02a69b1a0 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -50,11 +50,7 @@ Output.prototype.generate = function() {
// Load all plugins
.then(function() {
- that.log.info.ln('Loading and preparing plugins');
-
- var plugins = _.pluck(that.book.config.get('plugins'), 'name');
-
- return that.plugins.load(plugins);
+ return that.plugins.loadAll();
})
// Initialize the generation
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index f26c63d32..9642b4bb1 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -63,6 +63,14 @@ PluginsManager.prototype.load = function(name) {
.then(this._setup);
};
+// Load all plugins from the book's configuration
+PluginsManager.prototype.loadAll = function() {
+ var plugins = _.pluck(this.book.config.get('plugins'), 'name');
+
+ this.log.info.ln('installing', plugins.length, 'plugins');
+ return this.load(plugins);
+};
+
// Setup a plugin
// Register its filter, blocks, etc
PluginsManager.prototype._setup = function(plugin) {
@@ -71,7 +79,22 @@ PluginsManager.prototype._setup = function(plugin) {
// Install all plugins for the book
PluginsManager.prototype.install = function() {
- this.log.info.ln('installing 0 plugins');
+ var plugins = _.filter(this.book.config.get('plugins'), {
+ isDefault: false
+ });
+
+ if (plugins.length == 0) {
+ this.log.info.ln('nothing to install!');
+ return Promise(0);
+ }
+
+ this.log.info.ln('installing', plugins.length, 'plugins');
+
+ return Promise.serie(plugins, function(plugin) {
+ return registry.install(plugin.name, plugin.version);
+ })
+ .thenResolve(plugins.length);
+
};
module.exports = PluginsManager;
From e95678cf3a3cfbcbfa8b64ca16c4030417187e88 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 12:55:46 +0100
Subject: [PATCH 071/217] Add tests for installation of plugins
---
lib/plugins/index.js | 3 ++-
test/mock.js | 8 +++++++-
test/plugins.js | 38 ++++++++++++++++++++++++++++++++------
3 files changed, 41 insertions(+), 8 deletions(-)
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 9642b4bb1..6eb788b96 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -79,6 +79,7 @@ PluginsManager.prototype._setup = function(plugin) {
// Install all plugins for the book
PluginsManager.prototype.install = function() {
+ var that = this;
var plugins = _.filter(this.book.config.get('plugins'), {
isDefault: false
});
@@ -91,7 +92,7 @@ PluginsManager.prototype.install = function() {
this.log.info.ln('installing', plugins.length, 'plugins');
return Promise.serie(plugins, function(plugin) {
- return registry.install(plugin.name, plugin.version);
+ return registry.install(that.book, plugin.name, plugin.version);
})
.thenResolve(plugins.length);
diff --git a/test/mock.js b/test/mock.js
index ce4dca5df..701df9999 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -78,8 +78,14 @@ function outputDefaultBook(Output, files, summary, opts) {
});
}
+// Log an error
+function logError(err) {
+ console.log(err.stack || err);
+}
+
module.exports = {
setupBook: setupBook,
setupDefaultBook: setupDefaultBook,
- outputDefaultBook: outputDefaultBook
+ outputDefaultBook: outputDefaultBook,
+ logError: logError
};
diff --git a/test/plugins.js b/test/plugins.js
index 81c099e13..47f93143c 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -1,6 +1,7 @@
var mock = require('./mock');
var registry = require('../lib/plugins/registry');
var Output = require('../lib/output/base');
+var PluginsManager = require('../lib/plugins');
describe('Plugins', function() {
var book;
@@ -19,6 +20,15 @@ describe('Plugins', function() {
});
});
+ describe('Loading', function() {
+ it('should load default plugins', function() {
+ return mock.outputDefaultBook(Output)
+ .then(function(output) {
+ output.plugins.count().should.be.greaterThan(0);
+ });
+ });
+ });
+
describe('Installation', function() {
it('should install a plugin from NPM without a specific version', function() {
return registry.install(book, 'ga')
@@ -29,15 +39,31 @@ describe('Plugins', function() {
return registry.install(book, 'ga', '1.0.0')
.should.be.fulfilled();
});
- });
- describe('Loading', function() {
- it('should load default plugins', function() {
- return mock.outputDefaultBook(Output)
- .then(function(output) {
- output.plugins.count().should.be.greaterThan(0);
+ it('should correctly install all dependencies (if none)', function() {
+ return mock.setupBook({})
+ .then(function(book) {
+ var plugins = new PluginsManager(book);
+ return plugins.install()
+ .should.be.fulfilledWith(0);
});
});
+
+ it('should correctly install all dependencies (if any)', function() {
+ return mock.setupBook({
+ 'book.json': {
+ plugins: ['ga']
+ }
+ })
+ .then(function(book) {
+ return book.config.load()
+ .then(function() {
+ var plugins = new PluginsManager(book);
+ return plugins.install();
+ });
+ })
+ .should.be.fulfilledWith(1);
+ });
});
});
From b6104c64fecb72c9f40554600d456c27dbd18be8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 13:12:34 +0100
Subject: [PATCH 072/217] Use same json for page context and json builds
---
lib/cli/helper.js | 26 ++++++++++++++++++++++----
lib/cli/index.js | 23 ++++++++++++++++++++++-
lib/config/default.js | 5 -----
lib/output/base.js | 6 ++++++
lib/output/json.js | 10 ++++------
lib/page/index.js | 3 ++-
6 files changed, 56 insertions(+), 17 deletions(-)
diff --git a/lib/cli/helper.js b/lib/cli/helper.js
index 67bc5021b..61b9957d3 100644
--- a/lib/cli/helper.js
+++ b/lib/cli/helper.js
@@ -3,6 +3,9 @@ var _ = require('lodash');
var Book = require('../book');
var NodeFS = require('../fs/node');
var Logger = require('../utils/logger');
+var JSONOutput = require('../output/json');
+var WebsiteOutput = require('../output/website');
+var EBookOutput = require('../output/ebook');
var LOG_OPTION = {
name: 'log',
@@ -16,6 +19,19 @@ var LOG_OPTION = {
defaults: 'info'
};
+var FORMAT_OPTION = {
+ name: 'format',
+ description: 'Format to build to',
+ values: ['website', 'json', 'ebook'],
+ defaults: 'website'
+};
+
+var FORMATS = {
+ json: JSONOutput,
+ website: WebsiteOutput,
+ ebook: EBookOutput
+};
+
// Commands which is processing a book
// the root of the book is the first argument (or current directory)
function bookCmd(fn) {
@@ -29,13 +45,14 @@ function bookCmd(fn) {
logLevel: kwargs.log
});
- return fn(book, args.slice(1));
+ return fn(book, args.slice(1), kwargs);
};
}
// Commands which is working on a Output instance
-function outputCmd(Out, fn) {
- return bookCmd(function(book, args) {
+function outputCmd(fn) {
+ return bookCmd(function(book, args, kwargs) {
+ var Out = FORMATS[kwargs.format];
return fn(new Out(book), args);
});
}
@@ -45,6 +62,7 @@ module.exports = {
outputCmd: outputCmd,
options: {
- log: LOG_OPTION
+ log: LOG_OPTION,
+ format: FORMAT_OPTION
}
};
diff --git a/lib/cli/index.js b/lib/cli/index.js
index 4bb926bc3..c9a2e8f65 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -1,4 +1,5 @@
-var Output = require('../output/base');
+var path = require('path');
+
var PluginsManager = require('../plugins');
var helper = require('./helper');
@@ -39,6 +40,26 @@ module.exports = {
var plugins = new PluginsManager(book);
return plugins.install();
})
+ },
+
+ {
+ name: 'build [book] [output]',
+ description: 'build a book',
+ options: [
+ helper.options.log,
+ helper.options.format
+ ],
+ exec: helper.outputCmd(function(output, args, kwargs) {
+ return output.book.parse()
+ .then(function() {
+ // Set output folder
+ if (args[0]) {
+ output.book.config.set('output', path.resolve(process.cwd(), args[0]));
+ }
+
+ return output.generate();
+ });
+ })
}
diff --git a/lib/config/default.js b/lib/config/default.js
index 92defaac0..a35f1a526 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -1,11 +1,6 @@
var path = require('path');
module.exports = {
- // Options that can't be extend
- 'configFile': 'book',
- 'generator': 'website',
- 'extension': null,
-
// Book metadatas (somes are extracted from the README by default)
'title': null,
'description': null,
diff --git a/lib/output/base.js b/lib/output/base.js
index 02a69b1a0..997b8da2b 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -82,10 +82,12 @@ Output.prototype.generate = function() {
.value();
return Promise.serie(byTypes.asset, function(filename) {
+ that.log.info.ln('copy asset', filename);
return that.onAsset(filename);
})
.then(function() {
return Promise.serie(byTypes.page, function(filename) {
+ that.log.info.ln('process page', filename);
return that.onPage(that.book.getPage(filename));
});
});
@@ -94,6 +96,10 @@ Output.prototype.generate = function() {
// Finish the generation
.then(function() {
return that.finish();
+ })
+
+ .then(function() {
+ that.log.info.ln('generation finished with success!');
});
};
diff --git a/lib/output/json.js b/lib/output/json.js
index 913bc2b0b..9e6cb3c68 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,7 +1,7 @@
+var _ = require('lodash');
var gitbook = require('../gitbook');
var conrefsLoader = require('./conrefs');
-
var JSONOutput = conrefsLoader();
JSONOutput.prototype.name = 'json';
@@ -18,13 +18,11 @@ JSONOutput.prototype.onPage = function(page) {
// Write as json
.then(function() {
- var json = {
+ var json = _.extend(page.getContext(), {
gitbook: {
version: gitbook.version
- },
- path: page.path,
- sections: page.content
- };
+ }
+ });
return that.writeFile(
page.withExtension('.json'),
diff --git a/lib/page/index.js b/lib/page/index.js
index 342830a45..06b210ea3 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -118,7 +118,8 @@ Page.prototype.getContext = function() {
page: {
title: article? article.title : null,
next: next? next.getContext() : null,
- previous: prev? prev.getContext() : null
+ previous: prev? prev.getContext() : null,
+ content: this.content
}
};
};
From 853ba6079c37aa747721c8ee5d014479e69f3ad9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 13:30:45 +0100
Subject: [PATCH 073/217] Correctly clean and ignore output folder
---
lib/output/base.js | 23 +++++++++++------------
lib/output/folder.js | 27 ++++++++++++++++++++++++++-
lib/utils/fs.js | 11 ++++++++++-
package.json | 3 ++-
4 files changed, 49 insertions(+), 15 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 997b8da2b..6ac3d7eee 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -27,18 +27,6 @@ function Output(book) {
// Files to ignore in output
this.ignore = Ignore();
- this.ignore.addPattern(_.compact([
- '.gitignore',
- '.ignore',
- '.bookignore',
-
- // The configuration file should not be copied in the output
- this.book.config.path,
-
- // Structure file to ignore
- this.book.summary.path,
- this.book.langs.path
- ]));
}
// Start the generation, for a parsed book
@@ -105,7 +93,18 @@ Output.prototype.generate = function() {
// Prepare the generation
Output.prototype.prepare = function() {
+ this.ignore.addPattern(_.compact([
+ '.gitignore',
+ '.ignore',
+ '.bookignore',
+ // The configuration file should not be copied in the output
+ this.book.config.path,
+
+ // Structure file to ignore
+ this.book.summary.path,
+ this.book.langs.path
+ ]));
};
// Write a page (parsable file), ex: markdown, etc
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 9964ecd91..05843cd46 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -30,7 +30,32 @@ module.exports = function folderOutput(Base) {
// Prepare the generation by creating the output folder
FolderOutput.prototype.prepare = function() {
- return fs.mkdirp(this.root());
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ return Base.prototype.prepare.apply(that);
+ })
+
+ // Cleanup output folder
+ .then(function() {
+ return fs.rmDir(that.root())
+ .fail(function() {
+ return Promise();
+ });
+ })
+
+ // Create output folder
+ .then(function() {
+ return fs.mkdirp(that.root());
+ })
+
+ // Add output folder to ignored files
+ .then(function() {
+ that.ignore.addPattern([
+ path.relative(that.book.root, that.root())
+ ]);
+ });
};
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index 840d1f9cb..c2f2bd344 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -1,6 +1,7 @@
var fs = require('graceful-fs');
var mkdirp = require('mkdirp');
var destroy = require('destroy');
+var rmdir = require('rmdir');
var tmp = require('tmp');
var request = require('request');
var path = require('path');
@@ -95,6 +96,13 @@ function ensureFile(filename) {
return Promise.nfcall(mkdirp, base);
}
+// Remove a folder
+function rmDir(base) {
+ return Promise.nfcall(rmdir, base, {
+ fs: fs
+ });
+}
+
module.exports = {
exists: fileExists,
existsSync: fs.existsSync,
@@ -110,5 +118,6 @@ module.exports = {
tmpDir: genTmpDir,
download: download,
uniqueFilename: uniqueFilename,
- ensure: ensureFile
+ ensure: ensureFile,
+ rmDir: rmDir
};
diff --git a/package.json b/package.json
index 3eb60019a..84da7c04a 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,8 @@
"github-slugid": "1.0.0",
"destroy": "1.0.4",
"ignore": "2.2.19",
- "deprecated": "0.0.1"
+ "deprecated": "0.0.1",
+ "rmdir": "1.2.0"
},
"devDependencies": {
"eslint": "1.5.0",
From 96105d6ba0ac1bc86bd34088ae4763bf09b3c3ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 13:39:20 +0100
Subject: [PATCH 074/217] Extend templating context (and json output)
---
docs/variables.md | 3 +++
lib/book.js | 12 ++++++++++++
lib/gitbook.js | 14 +++++++++++++-
lib/output/json.js | 10 +++++-----
4 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/docs/variables.md b/docs/variables.md
index 35c7dd7e9..a0fc64e11 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -15,12 +15,15 @@ The following is a reference of the available data during book's parsing and the
| Variable | Description |
| -------- | ----------- |
+| `book.title` | Title specified in the `book.json` (or detected from the README) |
+| `book.description` | Description specified in the `book.json` (or detected from the README) |
| `book.[CONFIGURATION_DATA]` | All the `variables` set via the `book.json` are available through the book variable. |
### GitBook Variables
| Variable | Description |
| -------- | ----------- |
+| `gitbook.time` | The current time (when you run the `gitbook` command). |
| `gitbook.version` | Version of GitBook used to generate the book |
### File Variables
diff --git a/lib/book.js b/lib/book.js
index 173e9cce6..7928f248c 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -92,6 +92,18 @@ function Book(opts) {
_.bindAll(this);
}
+// Return templating context for the book
+Book.prototype.getContext = function() {
+ var variables = this.config.get('variables', {});
+
+ return {
+ book: _.extend({
+ title: this.config.get('title'),
+ description: this.config.get('description')
+ }, variables)
+ };
+};
+
// Parse and prepare the configuration, fail if invalid
Book.prototype.prepareConfig = function() {
return this.config.load();
diff --git a/lib/gitbook.js b/lib/gitbook.js
index cbdcaed9c..54513c165 100644
--- a/lib/gitbook.js
+++ b/lib/gitbook.js
@@ -4,6 +4,8 @@ var pkg = require('../package.json');
var VERSION = pkg.version;
var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
+var START_TIME = new Date();
+
// Verify that this gitbook version satisfies a requirement
// We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
function satisfies(condition) {
@@ -14,8 +16,18 @@ function satisfies(condition) {
return semver.satisfies(VERSION_STABLE, condition);
}
+// Return templating/json context for gitbook itself
+function getContext() {
+ return {
+ gitbook: {
+ version: pkg.version,
+ time: START_TIME
+ }
+ };
+}
module.exports = {
version: pkg.version,
- satisfies: satisfies
+ satisfies: satisfies,
+ getContext: getContext
};
diff --git a/lib/output/json.js b/lib/output/json.js
index 9e6cb3c68..c4317c974 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -18,11 +18,11 @@ JSONOutput.prototype.onPage = function(page) {
// Write as json
.then(function() {
- var json = _.extend(page.getContext(), {
- gitbook: {
- version: gitbook.version
- }
- });
+ var json = _.extend(
+ page.getContext(),
+ gitbook.getContext(),
+ that.book.getContext()
+ );
return that.writeFile(
page.withExtension('.json'),
From c0196c97a50786cd28c9b72197b39fefb47be333 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 13:59:52 +0100
Subject: [PATCH 075/217] Add context for summary
---
docs/variables.md | 8 ++++++++
lib/backbone/summary.js | 36 +++++++++++++++++++++++++++++++++++-
lib/output/base.js | 19 ++++++++++++++-----
lib/output/json.js | 7 +------
4 files changed, 58 insertions(+), 12 deletions(-)
diff --git a/docs/variables.md b/docs/variables.md
index a0fc64e11..bf2e88d14 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -10,6 +10,7 @@ The following is a reference of the available data during book's parsing and the
| `gitbook` | GitBook specific information |
| `page` | Current page specific information |
| `file` | File associated with the current page specific information |
+| `summary` | Information about the table of contents |
### Book Variables
@@ -41,6 +42,13 @@ The following is a reference of the available data during book's parsing and the
| `page.previous` | Previous page in the Table of Contents (can be `null`) |
| `page.next` | Next page in the Table of Contents (can be `null`) |
+#### Table of Contents Variables
+| Variable | Description |
+| -------- | ----------- |
+| `summary.parts` | List of sections in the Table of Contents |
+Thw whole table of contents (`SUMMARY.md`) can be accessed:
+
+`summary.parts[0].articles[0].title` will return the title of the first article.
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index cef3ae901..5d4d53cd5 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -45,7 +45,8 @@ TOCArticle.prototype.walk = function(iter) {
TOCArticle.prototype.getContext = function() {
return {
title: this.title,
- path: this.path
+ path: this.path,
+ anchor: this.anchor
};
};
@@ -98,6 +99,12 @@ TOCArticle.prototype.prev = function() {
}
};
+// 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.
*/
@@ -121,6 +128,11 @@ TOCPart.prototype.walk = function(iter) {
});
};
+// Map over all articles
+TOCPart.prototype.map = function(iter) {
+ return _.map(this.articles, iter);
+};
+
/*
A summary is composed of a list of parts, each composed wit a tree of articles.
*/
@@ -152,6 +164,28 @@ Summary.prototype.parse = function(content) {
});
};
+// 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 {
+ articles: part.map(onArticle)
+ };
+ })
+ }
+ };
+};
+
// Iterate over all entries of the summary
// iter is called with an TOCArticle
Summary.prototype.walk = function(iter) {
diff --git a/lib/output/base.js b/lib/output/base.js
index 6ac3d7eee..987179aef 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -5,6 +5,7 @@ var path = require('path');
var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
var TemplateEngine = require('../template');
+var gitbook = require('../gitbook');
/*
Output is like a stream interface for a parsed book
@@ -117,6 +118,11 @@ Output.prototype.onAsset = function(filename) {
};
+// Finish the generation
+Output.prototype.finish = function() {
+
+};
+
// Resolve an HTML link
Output.prototype.onRelativeLink = function(currentPage, href) {
var to = this.book.getPage(href);
@@ -146,13 +152,16 @@ Output.prototype.onResolveTemplate = function(from, to) {
return path.resolve(path.dirname(from), to);
};
-// Finish the generation
-Output.prototype.finish = function() {
-};
-
-Output.createMixin = function(def) {
+// ---- Utilities ----
+Output.prototype.getPageContext = function(page) {
+ return _.extend(
+ page.getContext(),
+ gitbook.getContext(),
+ this.book.getContext(),
+ this.book.summary.getContext()
+ );
};
module.exports = Output;
diff --git a/lib/output/json.js b/lib/output/json.js
index c4317c974..4fb3bbf67 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,5 +1,4 @@
var _ = require('lodash');
-var gitbook = require('../gitbook');
var conrefsLoader = require('./conrefs');
var JSONOutput = conrefsLoader();
@@ -18,11 +17,7 @@ JSONOutput.prototype.onPage = function(page) {
// Write as json
.then(function() {
- var json = _.extend(
- page.getContext(),
- gitbook.getContext(),
- that.book.getContext()
- );
+ var json = that.getPageContext(page);
return that.writeFile(
page.withExtension('.json'),
From 6ee9a3a64c76109719227a056ae2b29cb447cd76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 14:07:15 +0100
Subject: [PATCH 076/217] Remove old theme
---
.gitignore | 3 +-
lib/output/base.js | 1 +
theme/assets/ebook/ebook.css | 1 -
theme/assets/ebook/epub.css | 1 -
theme/assets/ebook/mobi.css | 1 -
theme/assets/ebook/pdf.css | 1 -
theme/assets/website/app.js | 25001 ----------------
.../website/fonts/fontawesome/FontAwesome.otf | Bin 75188 -> 0 bytes
.../fonts/fontawesome/fontawesome-webfont.eot | Bin 72449 -> 0 bytes
.../fonts/fontawesome/fontawesome-webfont.svg | 504 -
.../fonts/fontawesome/fontawesome-webfont.ttf | Bin 141564 -> 0 bytes
.../fontawesome/fontawesome-webfont.woff | Bin 83760 -> 0 bytes
.../apple-touch-icon-precomposed-152.png | Bin 92815 -> 0 bytes
theme/assets/website/images/favicon.ico | Bin 4286 -> 0 bytes
theme/assets/website/style.css | 9 -
theme/i18n/ar.json | 21 -
theme/i18n/bn.json | 20 -
theme/i18n/ca.json | 20 -
theme/i18n/cs.json | 20 -
theme/i18n/de.json | 20 -
theme/i18n/en.json | 20 -
theme/i18n/es.json | 20 -
theme/i18n/fa.json | 21 -
theme/i18n/fi.json | 20 -
theme/i18n/fr.json | 20 -
theme/i18n/he.json | 21 -
theme/i18n/it.json | 20 -
theme/i18n/ja.json | 20 -
theme/i18n/ko.json | 20 -
theme/i18n/no.json | 20 -
theme/i18n/pl.json | 20 -
theme/i18n/pt.json | 20 -
theme/i18n/ro.json | 20 -
theme/i18n/ru.json | 20 -
theme/i18n/sv.json | 20 -
theme/i18n/tr.json | 20 -
theme/i18n/uk.json | 20 -
theme/i18n/vi.json | 20 -
theme/i18n/zh-hans.json | 20 -
theme/i18n/zh-tw.json | 20 -
theme/javascript/dropdown.js | 25 -
theme/javascript/events.js | 4 -
theme/javascript/index.js | 84 -
theme/javascript/keyboard.js | 36 -
theme/javascript/loading.js | 14 -
theme/javascript/navigation.js | 165 -
theme/javascript/platform.js | 5 -
theme/javascript/sidebar.js | 56 -
theme/javascript/state.js | 38 -
theme/javascript/storage.js | 37 -
theme/javascript/toolbar.js | 181 -
theme/stylesheets/base/mixins.less | 32 -
theme/stylesheets/base/normalize.less | 396 -
theme/stylesheets/base/preboot.less | 406 -
theme/stylesheets/ebook.less | 1 -
theme/stylesheets/ebook/base.less | 23 -
theme/stylesheets/ebook/glossary.less | 28 -
theme/stylesheets/ebook/summary.less | 56 -
theme/stylesheets/ebook/variables.less | 5 -
theme/stylesheets/epub.less | 3 -
theme/stylesheets/mobi.less | 23 -
theme/stylesheets/pdf.less | 20 -
theme/stylesheets/website.less | 47 -
theme/stylesheets/website/alerts.less | 31 -
theme/stylesheets/website/body.less | 87 -
theme/stylesheets/website/buttons.less | 32 -
theme/stylesheets/website/dropdown.less | 105 -
theme/stylesheets/website/glossary.less | 22 -
theme/stylesheets/website/header.less | 82 -
theme/stylesheets/website/languages.less | 51 -
theme/stylesheets/website/markup.less | 12 -
theme/stylesheets/website/navigation.less | 50 -
theme/stylesheets/website/summary.less | 110 -
theme/stylesheets/website/variables.less | 107 -
theme/templates/ebook/footer.html | 8 -
theme/templates/ebook/glossary.html | 22 -
theme/templates/ebook/header.html | 7 -
theme/templates/ebook/layout.html | 24 -
theme/templates/ebook/page.html | 40 -
theme/templates/ebook/summary.html | 47 -
theme/templates/website/glossary.html | 17 -
theme/templates/website/includes/header.html | 12 -
theme/templates/website/includes/summary.html | 57 -
theme/templates/website/langs.html | 25 -
theme/templates/website/layout.html | 31 -
theme/templates/website/page.html | 82 -
86 files changed, 3 insertions(+), 28768 deletions(-)
delete mode 100644 theme/assets/ebook/ebook.css
delete mode 100644 theme/assets/ebook/epub.css
delete mode 100644 theme/assets/ebook/mobi.css
delete mode 100644 theme/assets/ebook/pdf.css
delete mode 100644 theme/assets/website/app.js
delete mode 100644 theme/assets/website/fonts/fontawesome/FontAwesome.otf
delete mode 100755 theme/assets/website/fonts/fontawesome/fontawesome-webfont.eot
delete mode 100755 theme/assets/website/fonts/fontawesome/fontawesome-webfont.svg
delete mode 100755 theme/assets/website/fonts/fontawesome/fontawesome-webfont.ttf
delete mode 100755 theme/assets/website/fonts/fontawesome/fontawesome-webfont.woff
delete mode 100644 theme/assets/website/images/apple-touch-icon-precomposed-152.png
delete mode 100644 theme/assets/website/images/favicon.ico
delete mode 100755 theme/assets/website/style.css
delete mode 100644 theme/i18n/ar.json
delete mode 100644 theme/i18n/bn.json
delete mode 100644 theme/i18n/ca.json
delete mode 100644 theme/i18n/cs.json
delete mode 100644 theme/i18n/de.json
delete mode 100644 theme/i18n/en.json
delete mode 100644 theme/i18n/es.json
delete mode 100644 theme/i18n/fa.json
delete mode 100644 theme/i18n/fi.json
delete mode 100644 theme/i18n/fr.json
delete mode 100644 theme/i18n/he.json
delete mode 100644 theme/i18n/it.json
delete mode 100644 theme/i18n/ja.json
delete mode 100644 theme/i18n/ko.json
delete mode 100644 theme/i18n/no.json
delete mode 100644 theme/i18n/pl.json
delete mode 100644 theme/i18n/pt.json
delete mode 100644 theme/i18n/ro.json
delete mode 100644 theme/i18n/ru.json
delete mode 100644 theme/i18n/sv.json
delete mode 100644 theme/i18n/tr.json
delete mode 100644 theme/i18n/uk.json
delete mode 100644 theme/i18n/vi.json
delete mode 100644 theme/i18n/zh-hans.json
delete mode 100644 theme/i18n/zh-tw.json
delete mode 100644 theme/javascript/dropdown.js
delete mode 100644 theme/javascript/events.js
delete mode 100755 theme/javascript/index.js
delete mode 100644 theme/javascript/keyboard.js
delete mode 100644 theme/javascript/loading.js
delete mode 100644 theme/javascript/navigation.js
delete mode 100644 theme/javascript/platform.js
delete mode 100644 theme/javascript/sidebar.js
delete mode 100644 theme/javascript/state.js
delete mode 100644 theme/javascript/storage.js
delete mode 100644 theme/javascript/toolbar.js
delete mode 100644 theme/stylesheets/base/mixins.less
delete mode 100644 theme/stylesheets/base/normalize.less
delete mode 100644 theme/stylesheets/base/preboot.less
delete mode 100644 theme/stylesheets/ebook.less
delete mode 100644 theme/stylesheets/ebook/base.less
delete mode 100644 theme/stylesheets/ebook/glossary.less
delete mode 100644 theme/stylesheets/ebook/summary.less
delete mode 100644 theme/stylesheets/ebook/variables.less
delete mode 100644 theme/stylesheets/epub.less
delete mode 100644 theme/stylesheets/mobi.less
delete mode 100644 theme/stylesheets/pdf.less
delete mode 100755 theme/stylesheets/website.less
delete mode 100644 theme/stylesheets/website/alerts.less
delete mode 100755 theme/stylesheets/website/body.less
delete mode 100644 theme/stylesheets/website/buttons.less
delete mode 100644 theme/stylesheets/website/dropdown.less
delete mode 100644 theme/stylesheets/website/glossary.less
delete mode 100755 theme/stylesheets/website/header.less
delete mode 100755 theme/stylesheets/website/languages.less
delete mode 100755 theme/stylesheets/website/markup.less
delete mode 100755 theme/stylesheets/website/navigation.less
delete mode 100755 theme/stylesheets/website/summary.less
delete mode 100644 theme/stylesheets/website/variables.less
delete mode 100644 theme/templates/ebook/footer.html
delete mode 100644 theme/templates/ebook/glossary.html
delete mode 100644 theme/templates/ebook/header.html
delete mode 100644 theme/templates/ebook/layout.html
delete mode 100644 theme/templates/ebook/page.html
delete mode 100644 theme/templates/ebook/summary.html
delete mode 100644 theme/templates/website/glossary.html
delete mode 100644 theme/templates/website/includes/header.html
delete mode 100644 theme/templates/website/includes/summary.html
delete mode 100755 theme/templates/website/langs.html
delete mode 100644 theme/templates/website/layout.html
delete mode 100644 theme/templates/website/page.html
diff --git a/.gitignore b/.gitignore
index 4fabbc930..79d58fcc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,4 +28,5 @@ node_modules
*.swp
lib2
-test2
\ No newline at end of file
+test2
+theme2
\ No newline at end of file
diff --git a/lib/output/base.js b/lib/output/base.js
index 987179aef..ad2d90d98 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -155,6 +155,7 @@ Output.prototype.onResolveTemplate = function(from, to) {
// ---- Utilities ----
+// Return the complete templating context for a page
Output.prototype.getPageContext = function(page) {
return _.extend(
page.getContext(),
diff --git a/theme/assets/ebook/ebook.css b/theme/assets/ebook/ebook.css
deleted file mode 100644
index b8d575479..000000000
--- a/theme/assets/ebook/ebook.css
+++ /dev/null
@@ -1 +0,0 @@
-.section.glossary>h1,.section.toc>h1{text-align:center}.page .section b,.page .section dl dt,.page .section strong,.page .section table th{font-weight:700}.link-inherit,.link-inherit:focus,.link-inherit:hover{color:inherit}.hidden{display:none}.section.toc>ol{margin:0;padding:0}.section.toc li{list-style:none}.section.toc li .inner{display:block;border-bottom:1px dotted #eee;margin-bottom:4px}.section.toc li .inner a,.section.toc li .inner span{padding-right:5px;border-bottom:1px solid #fff;margin-bottom:-1px}.section.toc li .inner .page{float:right;padding-left:5px}.section.toc li ol{margin:0;padding:0 0 0 2em}.dir-rtl .section.toc li .inner .page{float:left}.dir-rtl .section.toc li ol{margin:0;padding:0 2em 0 0}.section.glossary .glossary-entry{margin-bottom:40px}.section.glossary .glossary-entry h2 a,.section.glossary .glossary-entry h2 a:hover{color:inherit;text-decoration:none}.page .section,body{color:#000;text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.section.glossary .glossary-entry .glossary-index{list-style:none;margin:0;padding:0}.section.glossary .glossary-entry .glossary-index li{display:inline;margin:0 8px;white-space:nowrap}body{font-family:sans-serif}.page .section{display:block;word-wrap:break-word;overflow:hidden;line-height:1.6;-moz-text-size-adjust:100%}.page .section *{box-sizing:border-box;-webkit-box-sizing:border-box;font-size:inherit}.page .section>:first-child{margin-top:0!important}.page .section>:last-child{margin-bottom:0!important}.page .section blockquote,.page .section code,.page .section figure,.page .section img,.page .section pre,.page .section table,.page .section tr{page-break-inside:avoid}.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section p{orphans:3;widows:3}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5{page-break-after:avoid}.page .section em{font-style:italic}.page .section blockquote,.page .section dl,.page .section ol,.page .section p,.page .section table,.page .section ul{margin-top:0;margin-bottom:.8em}.page .section a{color:#4183c4;text-decoration:none;background:0 0}.page .section a:active,.page .section a:focus,.page .section a:hover{outline:0;text-decoration:underline}.page .section img{border:0;max-width:100%}.page .section hr{height:4px;padding:0;margin:1.6em 0;overflow:hidden;background-color:#e7e7e7;border:none}.page .section hr:after,.page .section hr:before{display:table;content:" "}.page .section hr:after{clear:both}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section h6{margin-top:1.2em;margin-bottom:.8em;font-weight:700}.page .section h1{font-size:2em}.page .section h2{font-size:1.75em}.page .section h3{font-size:1.5em}.page .section h4{font-size:1.25em}.page .section h5{font-size:1em}.page .section h6{font-size:1em;color:#777}.page .section code,.page .section pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;direction:ltr;border:none;color:inherit}.page .section pre{overflow:auto;word-wrap:normal;margin:0 0 1.2em;padding:.85em 1em;background:#f7f7f7}.page .section pre>code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;font-size:.85em;white-space:pre;background:0 0}.page .section pre>code:after,.page .section pre>code:before{content:normal}.page .section code{padding:.2em;margin:0;font-size:.85em;background-color:#f7f7f7}.page .section code:after,.page .section code:before{letter-spacing:-.2em;content:"\00a0"}.page .section table{display:table;width:100%;border-collapse:collapse;border-spacing:0;overflow:auto}.page .section table td,.page .section table th{padding:6px 13px;border:1px solid #ddd}.page .section table tr{background-color:#fff;border-top:1px solid #ccc}.page .section table tr:nth-child(2n){background-color:#f8f8f8}.page .section ol,.page .section ul{padding:0 0 0 2em;margin:0 0 .8em}.page .section ol ol,.page .section ol ul,.page .section ul ol,.page .section ul ul{margin-top:0;margin-bottom:0}.page .section ol ol{list-style-type:lower-roman}.page .section blockquote{margin:0 0 .8em;padding:0 15px;color:#858585;border-left:4px solid #e5e5e5}.page .section blockquote:first-child{margin-top:0}.page .section blockquote:last-child{margin-bottom:0}.page .section dl{padding:0}.page .section dl dt{padding:0;margin-top:.8em;font-style:italic}.page .section dl dd{padding:0 .8em;margin-bottom:.8em}.page .section dd{margin-left:0}.page .book-chapter{display:none}
\ No newline at end of file
diff --git a/theme/assets/ebook/epub.css b/theme/assets/ebook/epub.css
deleted file mode 100644
index 904983255..000000000
--- a/theme/assets/ebook/epub.css
+++ /dev/null
@@ -1 +0,0 @@
-.section.glossary>h1,.section.toc>h1{text-align:center}.page .section b,.page .section dl dt,.page .section strong,.page .section table th{font-weight:700}.link-inherit,.link-inherit:focus,.link-inherit:hover{color:inherit}.hidden{display:none}.section.toc>ol{margin:0;padding:0}.section.toc li{list-style:none}.section.toc li .inner{display:block;border-bottom:1px dotted #eee;margin-bottom:4px}.section.toc li .inner a,.section.toc li .inner span{padding-right:5px;border-bottom:1px solid #fff;margin-bottom:-1px}.section.toc li .inner .page{float:right;padding-left:5px}.section.toc li ol{margin:0;padding:0 0 0 2em}.dir-rtl .section.toc li .inner .page{float:left}.dir-rtl .section.toc li ol{margin:0;padding:0 2em 0 0}.section.glossary .glossary-entry{margin-bottom:40px}.section.glossary .glossary-entry h2 a,.section.glossary .glossary-entry h2 a:hover{color:inherit;text-decoration:none}.page .section,body{color:#000;text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.section.glossary .glossary-entry .glossary-index{list-style:none;margin:0;padding:0}.section.glossary .glossary-entry .glossary-index li{display:inline;margin:0 8px;white-space:nowrap}body{font-family:serif}.page .section{display:block;word-wrap:break-word;overflow:hidden;line-height:1.6;-moz-text-size-adjust:100%}.page .section *{box-sizing:border-box;-webkit-box-sizing:border-box;font-size:inherit}.page .section>:first-child{margin-top:0!important}.page .section>:last-child{margin-bottom:0!important}.page .section blockquote,.page .section code,.page .section figure,.page .section img,.page .section pre,.page .section table,.page .section tr{page-break-inside:avoid}.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section p{orphans:3;widows:3}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5{page-break-after:avoid}.page .section em{font-style:italic}.page .section blockquote,.page .section dl,.page .section ol,.page .section p,.page .section table,.page .section ul{margin-top:0;margin-bottom:.8em}.page .section a{color:#4183c4;text-decoration:none;background:0 0}.page .section a:active,.page .section a:focus,.page .section a:hover{outline:0;text-decoration:underline}.page .section img{border:0;max-width:100%}.page .section hr{height:4px;padding:0;margin:1.6em 0;overflow:hidden;background-color:#e7e7e7;border:none}.page .section hr:after,.page .section hr:before{display:table;content:" "}.page .section hr:after{clear:both}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section h6{margin-top:1.2em;margin-bottom:.8em;font-weight:700}.page .section h1{font-size:2em}.page .section h2{font-size:1.75em}.page .section h3{font-size:1.5em}.page .section h4{font-size:1.25em}.page .section h5{font-size:1em}.page .section h6{font-size:1em;color:#777}.page .section code,.page .section pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;direction:ltr;border:none;color:inherit}.page .section pre{overflow:auto;word-wrap:normal;margin:0 0 1.2em;padding:.85em 1em;background:#f7f7f7}.page .section pre>code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;font-size:.85em;white-space:pre;background:0 0}.page .section pre>code:after,.page .section pre>code:before{content:normal}.page .section code{padding:.2em;margin:0;font-size:.85em;background-color:#f7f7f7}.page .section code:after,.page .section code:before{letter-spacing:-.2em;content:"\00a0"}.page .section table{display:table;width:100%;border-collapse:collapse;border-spacing:0;overflow:auto}.page .section table td,.page .section table th{padding:6px 13px;border:1px solid #ddd}.page .section table tr{background-color:#fff;border-top:1px solid #ccc}.page .section table tr:nth-child(2n){background-color:#f8f8f8}.page .section ol,.page .section ul{padding:0 0 0 2em;margin:0 0 .8em}.page .section ol ol,.page .section ol ul,.page .section ul ol,.page .section ul ul{margin-top:0;margin-bottom:0}.page .section ol ol{list-style-type:lower-roman}.page .section blockquote{margin:0 0 .8em;padding:0 15px;color:#858585;border-left:4px solid #e5e5e5}.page .section blockquote:first-child{margin-top:0}.page .section blockquote:last-child{margin-bottom:0}.page .section dl{padding:0}.page .section dl dt{padding:0;margin-top:.8em;font-style:italic}.page .section dl dd{padding:0 .8em;margin-bottom:.8em}.page .section dd{margin-left:0}.page .book-chapter{display:none}
\ No newline at end of file
diff --git a/theme/assets/ebook/mobi.css b/theme/assets/ebook/mobi.css
deleted file mode 100644
index f3c903cc2..000000000
--- a/theme/assets/ebook/mobi.css
+++ /dev/null
@@ -1 +0,0 @@
-.section.glossary>h1,.section.toc>h1{text-align:center}.page .section,body{text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.page .section b,.page .section dl dt,.page .section strong,.page .section table th{font-weight:700}.link-inherit,.link-inherit:focus,.link-inherit:hover{color:inherit}.hidden{display:none}.section.toc>ol{margin:0;padding:0}.section.toc li{list-style:none}.section.toc li .inner{display:block;border-bottom:1px dotted #eee;margin-bottom:4px}.section.toc li .inner a,.section.toc li .inner span{padding-right:5px;border-bottom:1px solid #fff;margin-bottom:-1px}.section.toc li .inner .page{float:right;padding-left:5px}.section.toc li ol{margin:0;padding:0 0 0 2em}.dir-rtl .section.toc li .inner .page{float:left}.dir-rtl .section.toc li ol{margin:0;padding:0 2em 0 0}.section.glossary .glossary-entry{margin-bottom:40px}.section.glossary .glossary-entry h2 a,.section.glossary .glossary-entry h2 a:hover{color:inherit;text-decoration:none}.section.glossary .glossary-entry .glossary-index{list-style:none;margin:0;padding:0}.section.glossary .glossary-entry .glossary-index li{display:inline;margin:0 8px;white-space:nowrap}body{font-family:serif;color:#000}.page .section{display:block;word-wrap:break-word;overflow:hidden;color:#000;line-height:1.6;-moz-text-size-adjust:100%}.page .section *{box-sizing:border-box;-webkit-box-sizing:border-box;font-size:inherit}.page .section>:first-child{margin-top:0!important}.page .section>:last-child{margin-bottom:0!important}.page .section blockquote,.page .section code,.page .section figure,.page .section img,.page .section pre,.page .section table,.page .section tr{page-break-inside:avoid}.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section p{orphans:3;widows:3}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5{page-break-after:avoid}.page .section em{font-style:italic}.page .section blockquote,.page .section dl,.page .section ol,.page .section p,.page .section table,.page .section ul{margin-top:0;margin-bottom:.8em}.page .section a{color:#000;text-decoration:none;background:0 0}.page .section a:active,.page .section a:focus,.page .section a:hover{outline:0;text-decoration:underline}.page .section img{border:0;max-width:100%}.page .section hr{height:4px;padding:0;margin:1.6em 0;overflow:hidden;background-color:#e7e7e7;border:none}.page .section hr:after,.page .section hr:before{display:table;content:" "}.page .section hr:after{clear:both}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section h6{margin-top:1.2em;margin-bottom:.8em;font-weight:700}.page .section h1{font-size:2em}.page .section h2{font-size:1.75em}.page .section h3{font-size:1.5em}.page .section h4{font-size:1.25em}.page .section h5{font-size:1em}.page .section h6{font-size:1em;color:#777}.page .section code,.page .section pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;direction:ltr;border:none;color:inherit}.page .section pre{overflow:auto;word-wrap:normal;margin:0 0 1.2em;padding:.85em 1em;background:#f7f7f7}.page .section pre>code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;font-size:.85em;white-space:pre;background:0 0}.page .section pre>code:after,.page .section pre>code:before{content:normal}.page .section code{padding:.2em;margin:0;font-size:.85em;background-color:#f7f7f7}.page .section code:after,.page .section code:before{letter-spacing:-.2em;content:"\00a0"}.page .section table{display:table;width:100%;border-collapse:collapse;border-spacing:0;overflow:auto}.page .book-chapter,.page-toc li .inner span.page{display:none}.page .section table td,.page .section table th{padding:6px 13px;border:1px solid #ddd}.page .section table tr{background-color:#fff;border-top:1px solid #ccc}.page .section table tr:nth-child(2n){background-color:#f8f8f8}.page .section ol,.page .section ul{padding:0 0 0 2em;margin:0 0 .8em}.page .section ol ol,.page .section ol ul,.page .section ul ol,.page .section ul ul{margin-top:0;margin-bottom:0}.page .section ol ol{list-style-type:lower-roman}.page .section blockquote{margin:0 0 .8em;padding:0 15px;color:#858585;border-left:4px solid #e5e5e5}.page .section blockquote:first-child{margin-top:0}.page .section blockquote:last-child{margin-bottom:0}.page .section dl{padding:0}.page .section dl dt{padding:0;margin-top:.8em;font-style:italic}.page .section dl dd{padding:0 .8em;margin-bottom:.8em}.page .section dd{margin-left:0}a{color:#000;text-decoration:underline!important}.page-toc li{list-style-type:lower-alpha}.page-toc li .inner{border:none}
\ No newline at end of file
diff --git a/theme/assets/ebook/pdf.css b/theme/assets/ebook/pdf.css
deleted file mode 100644
index 10bb53e8f..000000000
--- a/theme/assets/ebook/pdf.css
+++ /dev/null
@@ -1 +0,0 @@
-.section.glossary>h1,.section.toc>h1{text-align:center}.page .section b,.page .section dl dt,.page .section strong,.page .section table th{font-weight:700}.link-inherit,.link-inherit:focus,.link-inherit:hover{color:inherit}.hidden{display:none}.section.toc>ol{margin:0;padding:0}.section.toc li{list-style:none}.section.toc li .inner{display:block;border-bottom:1px dotted #eee;margin-bottom:4px}.section.toc li .inner a,.section.toc li .inner span{padding-right:5px;border-bottom:1px solid #fff;margin-bottom:-1px}.section.toc li .inner .page{float:right;padding-left:5px}.section.toc li ol{margin:0;padding:0 0 0 2em}.dir-rtl .section.toc li .inner .page{float:left}.dir-rtl .section.toc li ol{margin:0;padding:0 2em 0 0}.section.glossary .glossary-entry{margin-bottom:40px}.section.glossary .glossary-entry h2 a,.section.glossary .glossary-entry h2 a:hover{color:inherit;text-decoration:none}.page .section,body{color:#000;text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.section.glossary .glossary-entry .glossary-index{list-style:none;margin:0;padding:0}.section.glossary .glossary-entry .glossary-index li{display:inline;margin:0 8px;white-space:nowrap}body{font-family:sans-serif}.page .section{display:block;word-wrap:break-word;overflow:hidden;line-height:1.6;-moz-text-size-adjust:100%}.page .section *{box-sizing:border-box;-webkit-box-sizing:border-box;font-size:inherit}.page .section>:first-child{margin-top:0!important}.page .section>:last-child{margin-bottom:0!important}.page .section blockquote,.page .section code,.page .section figure,.page .section img,.page .section pre,.page .section table,.page .section tr{page-break-inside:avoid}.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section p{orphans:3;widows:3}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5{page-break-after:avoid}.page .section em{font-style:italic}.page .section blockquote,.page .section dl,.page .section ol,.page .section p,.page .section table,.page .section ul{margin-top:0;margin-bottom:.8em}.page .section a{color:#4183c4;text-decoration:none;background:0 0}.page .section a:active,.page .section a:focus,.page .section a:hover{outline:0;text-decoration:underline}.page .section img{border:0;max-width:100%}.page .section hr{height:4px;padding:0;margin:1.6em 0;overflow:hidden;background-color:#e7e7e7;border:none}.page .section hr:after,.page .section hr:before{display:table;content:" "}.page .section hr:after{clear:both}.page .section h1,.page .section h2,.page .section h3,.page .section h4,.page .section h5,.page .section h6{margin-top:1.2em;margin-bottom:.8em;font-weight:700}.page .section h1{font-size:2em}.page .section h2{font-size:1.75em}.page .section h3{font-size:1.5em}.page .section h4{font-size:1.25em}.page .section h5{font-size:1em}.page .section h6{font-size:1em;color:#777}.page .section code,.page .section pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;direction:ltr;border:none;color:inherit}.page .section pre{overflow:auto;word-wrap:normal;margin:0 0 1.2em;padding:.85em 1em;background:#f7f7f7}.page .section pre>code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;font-size:.85em;white-space:pre;background:0 0}.page .section pre>code:after,.page .section pre>code:before{content:normal}.page .section code{padding:.2em;margin:0;font-size:.85em;background-color:#f7f7f7}.page .section code:after,.page .section code:before{letter-spacing:-.2em;content:"\00a0"}.page .section table{display:table;width:100%;border-collapse:collapse;border-spacing:0;overflow:auto}.page .section table td,.page .section table th{padding:6px 13px;border:1px solid #ddd}.page .section table tr{background-color:#fff;border-top:1px solid #ccc}.page .section table tr:nth-child(2n){background-color:#f8f8f8}.page .section ol,.page .section ul{padding:0 0 0 2em;margin:0 0 .8em}.page .section ol ol,.page .section ol ul,.page .section ul ol,.page .section ul ul{margin-top:0;margin-bottom:0}.page .section ol ol{list-style-type:lower-roman}.page .section blockquote{margin:0 0 .8em;padding:0 15px;color:#858585;border-left:4px solid #e5e5e5}.page .section blockquote:first-child{margin-top:0}.page .section blockquote:last-child{margin-bottom:0}.page .section dl{padding:0}.page .section dl dt{padding:0;margin-top:.8em;font-style:italic}.pdf-footer,.pdf-header{margin-top:20px;color:#aaa}.page .section dl dd{padding:0 .8em;margin-bottom:.8em}.page .section dd{margin-left:0}.page .book-chapter{display:none}.pdf-footer{padding-top:10px;border-top:1px solid #eee}.pdf-footer .footer-pages-count{float:right}.pdf-header{padding-bottom:10px;border-bottom:1px solid #eee}
\ No newline at end of file
diff --git a/theme/assets/website/app.js b/theme/assets/website/app.js
deleted file mode 100644
index 46a921bc4..000000000
--- a/theme/assets/website/app.js
+++ /dev/null
@@ -1,25001 +0,0 @@
-(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0 && j < len ? [ this[j] ] : [] );
- },
-
- end: function() {
- return this.prevObject || this.constructor(null);
- },
-
- // For internal use only.
- // Behaves like an Array's method, not like a jQuery method.
- push: push,
- sort: arr.sort,
- splice: arr.splice
-};
-
-jQuery.extend = jQuery.fn.extend = function() {
- var options, name, src, copy, copyIsArray, clone,
- target = arguments[0] || {},
- i = 1,
- length = arguments.length,
- deep = false;
-
- // Handle a deep copy situation
- if ( typeof target === "boolean" ) {
- deep = target;
-
- // Skip the boolean and the target
- target = arguments[ i ] || {};
- i++;
- }
-
- // Handle case when target is a string or something (possible in deep copy)
- if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
- target = {};
- }
-
- // Extend jQuery itself if only one argument is passed
- if ( i === length ) {
- target = this;
- i--;
- }
-
- for ( ; i < length; i++ ) {
- // Only deal with non-null/undefined values
- if ( (options = arguments[ i ]) != null ) {
- // Extend the base object
- for ( name in options ) {
- src = target[ name ];
- copy = options[ name ];
-
- // Prevent never-ending loop
- if ( target === copy ) {
- continue;
- }
-
- // Recurse if we're merging plain objects or arrays
- if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
- if ( copyIsArray ) {
- copyIsArray = false;
- clone = src && jQuery.isArray(src) ? src : [];
-
- } else {
- clone = src && jQuery.isPlainObject(src) ? src : {};
- }
-
- // Never move original objects, clone them
- target[ name ] = jQuery.extend( deep, clone, copy );
-
- // Don't bring in undefined values
- } else if ( copy !== undefined ) {
- target[ name ] = copy;
- }
- }
- }
- }
-
- // Return the modified object
- return target;
-};
-
-jQuery.extend({
- // Unique for each copy of jQuery on the page
- expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
-
- // Assume jQuery is ready without the ready module
- isReady: true,
-
- error: function( msg ) {
- throw new Error( msg );
- },
-
- noop: function() {},
-
- isFunction: function( obj ) {
- return jQuery.type(obj) === "function";
- },
-
- isArray: Array.isArray,
-
- isWindow: function( obj ) {
- return obj != null && obj === obj.window;
- },
-
- isNumeric: function( obj ) {
- // parseFloat NaNs numeric-cast false positives (null|true|false|"")
- // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
- // subtraction forces infinities to NaN
- // adding 1 corrects loss of precision from parseFloat (#15100)
- return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0;
- },
-
- isPlainObject: function( obj ) {
- // Not plain objects:
- // - Any object or value whose internal [[Class]] property is not "[object Object]"
- // - DOM nodes
- // - window
- if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
- return false;
- }
-
- if ( obj.constructor &&
- !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) {
- return false;
- }
-
- // If the function hasn't returned already, we're confident that
- // |obj| is a plain object, created by {} or constructed with new Object
- return true;
- },
-
- isEmptyObject: function( obj ) {
- var name;
- for ( name in obj ) {
- return false;
- }
- return true;
- },
-
- type: function( obj ) {
- if ( obj == null ) {
- return obj + "";
- }
- // Support: Android<4.0, iOS<6 (functionish RegExp)
- return typeof obj === "object" || typeof obj === "function" ?
- class2type[ toString.call(obj) ] || "object" :
- typeof obj;
- },
-
- // Evaluates a script in a global context
- globalEval: function( code ) {
- var script,
- indirect = eval;
-
- code = jQuery.trim( code );
-
- if ( code ) {
- // If the code includes a valid, prologue position
- // strict mode pragma, execute code by injecting a
- // script tag into the document.
- if ( code.indexOf("use strict") === 1 ) {
- script = document.createElement("script");
- script.text = code;
- document.head.appendChild( script ).parentNode.removeChild( script );
- } else {
- // Otherwise, avoid the DOM node creation, insertion
- // and removal by using an indirect global eval
- indirect( code );
- }
- }
- },
-
- // Convert dashed to camelCase; used by the css and data modules
- // Support: IE9-11+
- // Microsoft forgot to hump their vendor prefix (#9572)
- camelCase: function( string ) {
- return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
- },
-
- nodeName: function( elem, name ) {
- return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
- },
-
- // args is for internal usage only
- each: function( obj, callback, args ) {
- var value,
- i = 0,
- length = obj.length,
- isArray = isArraylike( obj );
-
- if ( args ) {
- if ( isArray ) {
- for ( ; i < length; i++ ) {
- value = callback.apply( obj[ i ], args );
-
- if ( value === false ) {
- break;
- }
- }
- } else {
- for ( i in obj ) {
- value = callback.apply( obj[ i ], args );
-
- if ( value === false ) {
- break;
- }
- }
- }
-
- // A special, fast, case for the most common use of each
- } else {
- if ( isArray ) {
- for ( ; i < length; i++ ) {
- value = callback.call( obj[ i ], i, obj[ i ] );
-
- if ( value === false ) {
- break;
- }
- }
- } else {
- for ( i in obj ) {
- value = callback.call( obj[ i ], i, obj[ i ] );
-
- if ( value === false ) {
- break;
- }
- }
- }
- }
-
- return obj;
- },
-
- // Support: Android<4.1
- trim: function( text ) {
- return text == null ?
- "" :
- ( text + "" ).replace( rtrim, "" );
- },
-
- // results is for internal usage only
- makeArray: function( arr, results ) {
- var ret = results || [];
-
- if ( arr != null ) {
- if ( isArraylike( Object(arr) ) ) {
- jQuery.merge( ret,
- typeof arr === "string" ?
- [ arr ] : arr
- );
- } else {
- push.call( ret, arr );
- }
- }
-
- return ret;
- },
-
- inArray: function( elem, arr, i ) {
- return arr == null ? -1 : indexOf.call( arr, elem, i );
- },
-
- merge: function( first, second ) {
- var len = +second.length,
- j = 0,
- i = first.length;
-
- for ( ; j < len; j++ ) {
- first[ i++ ] = second[ j ];
- }
-
- first.length = i;
-
- return first;
- },
-
- grep: function( elems, callback, invert ) {
- var callbackInverse,
- matches = [],
- i = 0,
- length = elems.length,
- callbackExpect = !invert;
-
- // Go through the array, only saving the items
- // that pass the validator function
- for ( ; i < length; i++ ) {
- callbackInverse = !callback( elems[ i ], i );
- if ( callbackInverse !== callbackExpect ) {
- matches.push( elems[ i ] );
- }
- }
-
- return matches;
- },
-
- // arg is for internal usage only
- map: function( elems, callback, arg ) {
- var value,
- i = 0,
- length = elems.length,
- isArray = isArraylike( elems ),
- ret = [];
-
- // Go through the array, translating each of the items to their new values
- if ( isArray ) {
- for ( ; i < length; i++ ) {
- value = callback( elems[ i ], i, arg );
-
- if ( value != null ) {
- ret.push( value );
- }
- }
-
- // Go through every key on the object,
- } else {
- for ( i in elems ) {
- value = callback( elems[ i ], i, arg );
-
- if ( value != null ) {
- ret.push( value );
- }
- }
- }
-
- // Flatten any nested arrays
- return concat.apply( [], ret );
- },
-
- // A global GUID counter for objects
- guid: 1,
-
- // Bind a function to a context, optionally partially applying any
- // arguments.
- proxy: function( fn, context ) {
- var tmp, args, proxy;
-
- if ( typeof context === "string" ) {
- tmp = fn[ context ];
- context = fn;
- fn = tmp;
- }
-
- // Quick check to determine if target is callable, in the spec
- // this throws a TypeError, but we will just return undefined.
- if ( !jQuery.isFunction( fn ) ) {
- return undefined;
- }
-
- // Simulated bind
- args = slice.call( arguments, 2 );
- proxy = function() {
- return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
- };
-
- // Set the guid of unique handler to the same of original handler, so it can be removed
- proxy.guid = fn.guid = fn.guid || jQuery.guid++;
-
- return proxy;
- },
-
- now: Date.now,
-
- // jQuery.support is not used in Core but other projects attach their
- // properties to it so it needs to exist.
- support: support
-});
-
-// Populate the class2type map
-jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
- class2type[ "[object " + name + "]" ] = name.toLowerCase();
-});
-
-function isArraylike( obj ) {
-
- // Support: iOS 8.2 (not reproducible in simulator)
- // `in` check used to prevent JIT error (gh-2145)
- // hasOwn isn't used here due to false negatives
- // regarding Nodelist length in IE
- var length = "length" in obj && obj.length,
- type = jQuery.type( obj );
-
- if ( type === "function" || jQuery.isWindow( obj ) ) {
- return false;
- }
-
- if ( obj.nodeType === 1 && length ) {
- return true;
- }
-
- return type === "array" || length === 0 ||
- typeof length === "number" && length > 0 && ( length - 1 ) in obj;
-}
-var Sizzle =
-/*!
- * Sizzle CSS Selector Engine v2.2.0-pre
- * http://sizzlejs.com/
- *
- * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
- * Released under the MIT license
- * http://jquery.org/license
- *
- * Date: 2014-12-16
- */
-(function( window ) {
-
-var i,
- support,
- Expr,
- getText,
- isXML,
- tokenize,
- compile,
- select,
- outermostContext,
- sortInput,
- hasDuplicate,
-
- // Local document vars
- setDocument,
- document,
- docElem,
- documentIsHTML,
- rbuggyQSA,
- rbuggyMatches,
- matches,
- contains,
-
- // Instance-specific data
- expando = "sizzle" + 1 * new Date(),
- preferredDoc = window.document,
- dirruns = 0,
- done = 0,
- classCache = createCache(),
- tokenCache = createCache(),
- compilerCache = createCache(),
- sortOrder = function( a, b ) {
- if ( a === b ) {
- hasDuplicate = true;
- }
- return 0;
- },
-
- // General-purpose constants
- MAX_NEGATIVE = 1 << 31,
-
- // Instance methods
- hasOwn = ({}).hasOwnProperty,
- arr = [],
- pop = arr.pop,
- push_native = arr.push,
- push = arr.push,
- slice = arr.slice,
- // Use a stripped-down indexOf as it's faster than native
- // http://jsperf.com/thor-indexof-vs-for/5
- indexOf = function( list, elem ) {
- var i = 0,
- len = list.length;
- for ( ; i < len; i++ ) {
- if ( list[i] === elem ) {
- return i;
- }
- }
- return -1;
- },
-
- booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
-
- // Regular expressions
-
- // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
- whitespace = "[\\x20\\t\\r\\n\\f]",
- // http://www.w3.org/TR/css3-syntax/#characters
- characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
-
- // Loosely modeled on CSS identifier characters
- // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
- // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
- identifier = characterEncoding.replace( "w", "w#" ),
-
- // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
- attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace +
- // Operator (capture 2)
- "*([*^$|!~]?=)" + whitespace +
- // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
- "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
- "*\\]",
-
- pseudos = ":(" + characterEncoding + ")(?:\\((" +
- // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
- // 1. quoted (capture 3; capture 4 or capture 5)
- "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
- // 2. simple (capture 6)
- "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
- // 3. anything else (capture 2)
- ".*" +
- ")\\)|)",
-
- // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
- rwhitespace = new RegExp( whitespace + "+", "g" ),
- rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
-
- rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
- rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
-
- rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
-
- rpseudo = new RegExp( pseudos ),
- ridentifier = new RegExp( "^" + identifier + "$" ),
-
- matchExpr = {
- "ID": new RegExp( "^#(" + characterEncoding + ")" ),
- "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
- "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
- "ATTR": new RegExp( "^" + attributes ),
- "PSEUDO": new RegExp( "^" + pseudos ),
- "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
- "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
- "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
- "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
- // For use in libraries implementing .is()
- // We use this for POS matching in `select`
- "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
- whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
- },
-
- rinputs = /^(?:input|select|textarea|button)$/i,
- rheader = /^h\d$/i,
-
- rnative = /^[^{]+\{\s*\[native \w/,
-
- // Easily-parseable/retrievable ID or TAG or CLASS selectors
- rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
-
- rsibling = /[+~]/,
- rescape = /'|\\/g,
-
- // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
- runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
- funescape = function( _, escaped, escapedWhitespace ) {
- var high = "0x" + escaped - 0x10000;
- // NaN means non-codepoint
- // Support: Firefox<24
- // Workaround erroneous numeric interpretation of +"0x"
- return high !== high || escapedWhitespace ?
- escaped :
- high < 0 ?
- // BMP codepoint
- String.fromCharCode( high + 0x10000 ) :
- // Supplemental Plane codepoint (surrogate pair)
- String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
- },
-
- // Used for iframes
- // See setDocument()
- // Removing the function wrapper causes a "Permission Denied"
- // error in IE
- unloadHandler = function() {
- setDocument();
- };
-
-// Optimize for push.apply( _, NodeList )
-try {
- push.apply(
- (arr = slice.call( preferredDoc.childNodes )),
- preferredDoc.childNodes
- );
- // Support: Android<4.0
- // Detect silently failing push.apply
- arr[ preferredDoc.childNodes.length ].nodeType;
-} catch ( e ) {
- push = { apply: arr.length ?
-
- // Leverage slice if possible
- function( target, els ) {
- push_native.apply( target, slice.call(els) );
- } :
-
- // Support: IE<9
- // Otherwise append directly
- function( target, els ) {
- var j = target.length,
- i = 0;
- // Can't trust NodeList.length
- while ( (target[j++] = els[i++]) ) {}
- target.length = j - 1;
- }
- };
-}
-
-function Sizzle( selector, context, results, seed ) {
- var match, elem, m, nodeType,
- // QSA vars
- i, groups, old, nid, newContext, newSelector;
-
- if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
- setDocument( context );
- }
-
- context = context || document;
- results = results || [];
- nodeType = context.nodeType;
-
- if ( typeof selector !== "string" || !selector ||
- nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
-
- return results;
- }
-
- if ( !seed && documentIsHTML ) {
-
- // Try to shortcut find operations when possible (e.g., not under DocumentFragment)
- if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
- // Speed-up: Sizzle("#ID")
- if ( (m = match[1]) ) {
- if ( nodeType === 9 ) {
- elem = context.getElementById( m );
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document (jQuery #6963)
- if ( elem && elem.parentNode ) {
- // Handle the case where IE, Opera, and Webkit return items
- // by name instead of ID
- if ( elem.id === m ) {
- results.push( elem );
- return results;
- }
- } else {
- return results;
- }
- } else {
- // Context is not a document
- if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
- contains( context, elem ) && elem.id === m ) {
- results.push( elem );
- return results;
- }
- }
-
- // Speed-up: Sizzle("TAG")
- } else if ( match[2] ) {
- push.apply( results, context.getElementsByTagName( selector ) );
- return results;
-
- // Speed-up: Sizzle(".CLASS")
- } else if ( (m = match[3]) && support.getElementsByClassName ) {
- push.apply( results, context.getElementsByClassName( m ) );
- return results;
- }
- }
-
- // QSA path
- if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
- nid = old = expando;
- newContext = context;
- newSelector = nodeType !== 1 && selector;
-
- // qSA works strangely on Element-rooted queries
- // We can work around this by specifying an extra ID on the root
- // and working up from there (Thanks to Andrew Dupont for the technique)
- // IE 8 doesn't work on object elements
- if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
- groups = tokenize( selector );
-
- if ( (old = context.getAttribute("id")) ) {
- nid = old.replace( rescape, "\\$&" );
- } else {
- context.setAttribute( "id", nid );
- }
- nid = "[id='" + nid + "'] ";
-
- i = groups.length;
- while ( i-- ) {
- groups[i] = nid + toSelector( groups[i] );
- }
- newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
- newSelector = groups.join(",");
- }
-
- if ( newSelector ) {
- try {
- push.apply( results,
- newContext.querySelectorAll( newSelector )
- );
- return results;
- } catch(qsaError) {
- } finally {
- if ( !old ) {
- context.removeAttribute("id");
- }
- }
- }
- }
- }
-
- // All others
- return select( selector.replace( rtrim, "$1" ), context, results, seed );
-}
-
-/**
- * Create key-value caches of limited size
- * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
- * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
- * deleting the oldest entry
- */
-function createCache() {
- var keys = [];
-
- function cache( key, value ) {
- // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
- if ( keys.push( key + " " ) > Expr.cacheLength ) {
- // Only keep the most recent entries
- delete cache[ keys.shift() ];
- }
- return (cache[ key + " " ] = value);
- }
- return cache;
-}
-
-/**
- * Mark a function for special use by Sizzle
- * @param {Function} fn The function to mark
- */
-function markFunction( fn ) {
- fn[ expando ] = true;
- return fn;
-}
-
-/**
- * Support testing using an element
- * @param {Function} fn Passed the created div and expects a boolean result
- */
-function assert( fn ) {
- var div = document.createElement("div");
-
- try {
- return !!fn( div );
- } catch (e) {
- return false;
- } finally {
- // Remove from its parent by default
- if ( div.parentNode ) {
- div.parentNode.removeChild( div );
- }
- // release memory in IE
- div = null;
- }
-}
-
-/**
- * Adds the same handler for all of the specified attrs
- * @param {String} attrs Pipe-separated list of attributes
- * @param {Function} handler The method that will be applied
- */
-function addHandle( attrs, handler ) {
- var arr = attrs.split("|"),
- i = attrs.length;
-
- while ( i-- ) {
- Expr.attrHandle[ arr[i] ] = handler;
- }
-}
-
-/**
- * Checks document order of two siblings
- * @param {Element} a
- * @param {Element} b
- * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
- */
-function siblingCheck( a, b ) {
- var cur = b && a,
- diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
- ( ~b.sourceIndex || MAX_NEGATIVE ) -
- ( ~a.sourceIndex || MAX_NEGATIVE );
-
- // Use IE sourceIndex if available on both nodes
- if ( diff ) {
- return diff;
- }
-
- // Check if b follows a
- if ( cur ) {
- while ( (cur = cur.nextSibling) ) {
- if ( cur === b ) {
- return -1;
- }
- }
- }
-
- return a ? 1 : -1;
-}
-
-/**
- * Returns a function to use in pseudos for input types
- * @param {String} type
- */
-function createInputPseudo( type ) {
- return function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return name === "input" && elem.type === type;
- };
-}
-
-/**
- * Returns a function to use in pseudos for buttons
- * @param {String} type
- */
-function createButtonPseudo( type ) {
- return function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return (name === "input" || name === "button") && elem.type === type;
- };
-}
-
-/**
- * Returns a function to use in pseudos for positionals
- * @param {Function} fn
- */
-function createPositionalPseudo( fn ) {
- return markFunction(function( argument ) {
- argument = +argument;
- return markFunction(function( seed, matches ) {
- var j,
- matchIndexes = fn( [], seed.length, argument ),
- i = matchIndexes.length;
-
- // Match elements found at the specified indexes
- while ( i-- ) {
- if ( seed[ (j = matchIndexes[i]) ] ) {
- seed[j] = !(matches[j] = seed[j]);
- }
- }
- });
- });
-}
-
-/**
- * Checks a node for validity as a Sizzle context
- * @param {Element|Object=} context
- * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
- */
-function testContext( context ) {
- return context && typeof context.getElementsByTagName !== "undefined" && context;
-}
-
-// Expose support vars for convenience
-support = Sizzle.support = {};
-
-/**
- * Detects XML nodes
- * @param {Element|Object} elem An element or a document
- * @returns {Boolean} True iff elem is a non-HTML XML node
- */
-isXML = Sizzle.isXML = function( elem ) {
- // documentElement is verified for cases where it doesn't yet exist
- // (such as loading iframes in IE - #4833)
- var documentElement = elem && (elem.ownerDocument || elem).documentElement;
- return documentElement ? documentElement.nodeName !== "HTML" : false;
-};
-
-/**
- * Sets document-related variables once based on the current document
- * @param {Element|Object} [doc] An element or document object to use to set the document
- * @returns {Object} Returns the current document
- */
-setDocument = Sizzle.setDocument = function( node ) {
- var hasCompare, parent,
- doc = node ? node.ownerDocument || node : preferredDoc;
-
- // If no document and documentElement is available, return
- if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
- return document;
- }
-
- // Set our document
- document = doc;
- docElem = doc.documentElement;
- parent = doc.defaultView;
-
- // Support: IE>8
- // If iframe document is assigned to "document" variable and if iframe has been reloaded,
- // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
- // IE6-8 do not support the defaultView property so parent will be undefined
- if ( parent && parent !== parent.top ) {
- // IE11 does not have attachEvent, so all must suffer
- if ( parent.addEventListener ) {
- parent.addEventListener( "unload", unloadHandler, false );
- } else if ( parent.attachEvent ) {
- parent.attachEvent( "onunload", unloadHandler );
- }
- }
-
- /* Support tests
- ---------------------------------------------------------------------- */
- documentIsHTML = !isXML( doc );
-
- /* Attributes
- ---------------------------------------------------------------------- */
-
- // Support: IE<8
- // Verify that getAttribute really returns attributes and not properties
- // (excepting IE8 booleans)
- support.attributes = assert(function( div ) {
- div.className = "i";
- return !div.getAttribute("className");
- });
-
- /* getElement(s)By*
- ---------------------------------------------------------------------- */
-
- // Check if getElementsByTagName("*") returns only elements
- support.getElementsByTagName = assert(function( div ) {
- div.appendChild( doc.createComment("") );
- return !div.getElementsByTagName("*").length;
- });
-
- // Support: IE<9
- support.getElementsByClassName = rnative.test( doc.getElementsByClassName );
-
- // Support: IE<10
- // Check if getElementById returns elements by name
- // The broken getElementById methods don't pick up programatically-set names,
- // so use a roundabout getElementsByName test
- support.getById = assert(function( div ) {
- docElem.appendChild( div ).id = expando;
- return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
- });
-
- // ID find and filter
- if ( support.getById ) {
- Expr.find["ID"] = function( id, context ) {
- if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
- var m = context.getElementById( id );
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document #6963
- return m && m.parentNode ? [ m ] : [];
- }
- };
- Expr.filter["ID"] = function( id ) {
- var attrId = id.replace( runescape, funescape );
- return function( elem ) {
- return elem.getAttribute("id") === attrId;
- };
- };
- } else {
- // Support: IE6/7
- // getElementById is not reliable as a find shortcut
- delete Expr.find["ID"];
-
- Expr.filter["ID"] = function( id ) {
- var attrId = id.replace( runescape, funescape );
- return function( elem ) {
- var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
- return node && node.value === attrId;
- };
- };
- }
-
- // Tag
- Expr.find["TAG"] = support.getElementsByTagName ?
- function( tag, context ) {
- if ( typeof context.getElementsByTagName !== "undefined" ) {
- return context.getElementsByTagName( tag );
-
- // DocumentFragment nodes don't have gEBTN
- } else if ( support.qsa ) {
- return context.querySelectorAll( tag );
- }
- } :
-
- function( tag, context ) {
- var elem,
- tmp = [],
- i = 0,
- // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
- results = context.getElementsByTagName( tag );
-
- // Filter out possible comments
- if ( tag === "*" ) {
- while ( (elem = results[i++]) ) {
- if ( elem.nodeType === 1 ) {
- tmp.push( elem );
- }
- }
-
- return tmp;
- }
- return results;
- };
-
- // Class
- Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
- if ( documentIsHTML ) {
- return context.getElementsByClassName( className );
- }
- };
-
- /* QSA/matchesSelector
- ---------------------------------------------------------------------- */
-
- // QSA and matchesSelector support
-
- // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
- rbuggyMatches = [];
-
- // qSa(:focus) reports false when true (Chrome 21)
- // We allow this because of a bug in IE8/9 that throws an error
- // whenever `document.activeElement` is accessed on an iframe
- // So, we allow :focus to pass through QSA all the time to avoid the IE error
- // See http://bugs.jquery.com/ticket/13378
- rbuggyQSA = [];
-
- if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
- // Build QSA regex
- // Regex strategy adopted from Diego Perini
- assert(function( div ) {
- // Select is set to empty string on purpose
- // This is to test IE's treatment of not explicitly
- // setting a boolean content attribute,
- // since its presence should be enough
- // http://bugs.jquery.com/ticket/12359
- docElem.appendChild( div ).innerHTML = " " +
- "" +
- " ";
-
- // Support: IE8, Opera 11-12.16
- // Nothing should be selected when empty strings follow ^= or $= or *=
- // The test attribute must be unknown in Opera but "safe" for WinRT
- // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
- if ( div.querySelectorAll("[msallowcapture^='']").length ) {
- rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
- }
-
- // Support: IE8
- // Boolean attributes and "value" are not treated correctly
- if ( !div.querySelectorAll("[selected]").length ) {
- rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
- }
-
- // Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+
- if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
- rbuggyQSA.push("~=");
- }
-
- // Webkit/Opera - :checked should return selected option elements
- // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
- // IE8 throws error here and will not see later tests
- if ( !div.querySelectorAll(":checked").length ) {
- rbuggyQSA.push(":checked");
- }
-
- // Support: Safari 8+, iOS 8+
- // https://bugs.webkit.org/show_bug.cgi?id=136851
- // In-page `selector#id sibing-combinator selector` fails
- if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) {
- rbuggyQSA.push(".#.+[+~]");
- }
- });
-
- assert(function( div ) {
- // Support: Windows 8 Native Apps
- // The type and name attributes are restricted during .innerHTML assignment
- var input = doc.createElement("input");
- input.setAttribute( "type", "hidden" );
- div.appendChild( input ).setAttribute( "name", "D" );
-
- // Support: IE8
- // Enforce case-sensitivity of name attribute
- if ( div.querySelectorAll("[name=d]").length ) {
- rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
- }
-
- // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
- // IE8 throws error here and will not see later tests
- if ( !div.querySelectorAll(":enabled").length ) {
- rbuggyQSA.push( ":enabled", ":disabled" );
- }
-
- // Opera 10-11 does not throw on post-comma invalid pseudos
- div.querySelectorAll("*,:x");
- rbuggyQSA.push(",.*:");
- });
- }
-
- if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
- docElem.webkitMatchesSelector ||
- docElem.mozMatchesSelector ||
- docElem.oMatchesSelector ||
- docElem.msMatchesSelector) )) ) {
-
- assert(function( div ) {
- // Check to see if it's possible to do matchesSelector
- // on a disconnected node (IE 9)
- support.disconnectedMatch = matches.call( div, "div" );
-
- // This should fail with an exception
- // Gecko does not error, returns false instead
- matches.call( div, "[s!='']:x" );
- rbuggyMatches.push( "!=", pseudos );
- });
- }
-
- rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
- rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
-
- /* Contains
- ---------------------------------------------------------------------- */
- hasCompare = rnative.test( docElem.compareDocumentPosition );
-
- // Element contains another
- // Purposefully does not implement inclusive descendent
- // As in, an element does not contain itself
- contains = hasCompare || rnative.test( docElem.contains ) ?
- function( a, b ) {
- var adown = a.nodeType === 9 ? a.documentElement : a,
- bup = b && b.parentNode;
- return a === bup || !!( bup && bup.nodeType === 1 && (
- adown.contains ?
- adown.contains( bup ) :
- a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
- ));
- } :
- function( a, b ) {
- if ( b ) {
- while ( (b = b.parentNode) ) {
- if ( b === a ) {
- return true;
- }
- }
- }
- return false;
- };
-
- /* Sorting
- ---------------------------------------------------------------------- */
-
- // Document order sorting
- sortOrder = hasCompare ?
- function( a, b ) {
-
- // Flag for duplicate removal
- if ( a === b ) {
- hasDuplicate = true;
- return 0;
- }
-
- // Sort on method existence if only one input has compareDocumentPosition
- var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
- if ( compare ) {
- return compare;
- }
-
- // Calculate position if both inputs belong to the same document
- compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
- a.compareDocumentPosition( b ) :
-
- // Otherwise we know they are disconnected
- 1;
-
- // Disconnected nodes
- if ( compare & 1 ||
- (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
-
- // Choose the first element that is related to our preferred document
- if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
- return -1;
- }
- if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
- return 1;
- }
-
- // Maintain original order
- return sortInput ?
- ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
- 0;
- }
-
- return compare & 4 ? -1 : 1;
- } :
- function( a, b ) {
- // Exit early if the nodes are identical
- if ( a === b ) {
- hasDuplicate = true;
- return 0;
- }
-
- var cur,
- i = 0,
- aup = a.parentNode,
- bup = b.parentNode,
- ap = [ a ],
- bp = [ b ];
-
- // Parentless nodes are either documents or disconnected
- if ( !aup || !bup ) {
- return a === doc ? -1 :
- b === doc ? 1 :
- aup ? -1 :
- bup ? 1 :
- sortInput ?
- ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
- 0;
-
- // If the nodes are siblings, we can do a quick check
- } else if ( aup === bup ) {
- return siblingCheck( a, b );
- }
-
- // Otherwise we need full lists of their ancestors for comparison
- cur = a;
- while ( (cur = cur.parentNode) ) {
- ap.unshift( cur );
- }
- cur = b;
- while ( (cur = cur.parentNode) ) {
- bp.unshift( cur );
- }
-
- // Walk down the tree looking for a discrepancy
- while ( ap[i] === bp[i] ) {
- i++;
- }
-
- return i ?
- // Do a sibling check if the nodes have a common ancestor
- siblingCheck( ap[i], bp[i] ) :
-
- // Otherwise nodes in our document sort first
- ap[i] === preferredDoc ? -1 :
- bp[i] === preferredDoc ? 1 :
- 0;
- };
-
- return doc;
-};
-
-Sizzle.matches = function( expr, elements ) {
- return Sizzle( expr, null, null, elements );
-};
-
-Sizzle.matchesSelector = function( elem, expr ) {
- // Set document vars if needed
- if ( ( elem.ownerDocument || elem ) !== document ) {
- setDocument( elem );
- }
-
- // Make sure that attribute selectors are quoted
- expr = expr.replace( rattributeQuotes, "='$1']" );
-
- if ( support.matchesSelector && documentIsHTML &&
- ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
- ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
-
- try {
- var ret = matches.call( elem, expr );
-
- // IE 9's matchesSelector returns false on disconnected nodes
- if ( ret || support.disconnectedMatch ||
- // As well, disconnected nodes are said to be in a document
- // fragment in IE 9
- elem.document && elem.document.nodeType !== 11 ) {
- return ret;
- }
- } catch (e) {}
- }
-
- return Sizzle( expr, document, null, [ elem ] ).length > 0;
-};
-
-Sizzle.contains = function( context, elem ) {
- // Set document vars if needed
- if ( ( context.ownerDocument || context ) !== document ) {
- setDocument( context );
- }
- return contains( context, elem );
-};
-
-Sizzle.attr = function( elem, name ) {
- // Set document vars if needed
- if ( ( elem.ownerDocument || elem ) !== document ) {
- setDocument( elem );
- }
-
- var fn = Expr.attrHandle[ name.toLowerCase() ],
- // Don't get fooled by Object.prototype properties (jQuery #13807)
- val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
- fn( elem, name, !documentIsHTML ) :
- undefined;
-
- return val !== undefined ?
- val :
- support.attributes || !documentIsHTML ?
- elem.getAttribute( name ) :
- (val = elem.getAttributeNode(name)) && val.specified ?
- val.value :
- null;
-};
-
-Sizzle.error = function( msg ) {
- throw new Error( "Syntax error, unrecognized expression: " + msg );
-};
-
-/**
- * Document sorting and removing duplicates
- * @param {ArrayLike} results
- */
-Sizzle.uniqueSort = function( results ) {
- var elem,
- duplicates = [],
- j = 0,
- i = 0;
-
- // Unless we *know* we can detect duplicates, assume their presence
- hasDuplicate = !support.detectDuplicates;
- sortInput = !support.sortStable && results.slice( 0 );
- results.sort( sortOrder );
-
- if ( hasDuplicate ) {
- while ( (elem = results[i++]) ) {
- if ( elem === results[ i ] ) {
- j = duplicates.push( i );
- }
- }
- while ( j-- ) {
- results.splice( duplicates[ j ], 1 );
- }
- }
-
- // Clear input after sorting to release objects
- // See https://github.com/jquery/sizzle/pull/225
- sortInput = null;
-
- return results;
-};
-
-/**
- * Utility function for retrieving the text value of an array of DOM nodes
- * @param {Array|Element} elem
- */
-getText = Sizzle.getText = function( elem ) {
- var node,
- ret = "",
- i = 0,
- nodeType = elem.nodeType;
-
- if ( !nodeType ) {
- // If no nodeType, this is expected to be an array
- while ( (node = elem[i++]) ) {
- // Do not traverse comment nodes
- ret += getText( node );
- }
- } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
- // Use textContent for elements
- // innerText usage removed for consistency of new lines (jQuery #11153)
- if ( typeof elem.textContent === "string" ) {
- return elem.textContent;
- } else {
- // Traverse its children
- for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
- ret += getText( elem );
- }
- }
- } else if ( nodeType === 3 || nodeType === 4 ) {
- return elem.nodeValue;
- }
- // Do not include comment or processing instruction nodes
-
- return ret;
-};
-
-Expr = Sizzle.selectors = {
-
- // Can be adjusted by the user
- cacheLength: 50,
-
- createPseudo: markFunction,
-
- match: matchExpr,
-
- attrHandle: {},
-
- find: {},
-
- relative: {
- ">": { dir: "parentNode", first: true },
- " ": { dir: "parentNode" },
- "+": { dir: "previousSibling", first: true },
- "~": { dir: "previousSibling" }
- },
-
- preFilter: {
- "ATTR": function( match ) {
- match[1] = match[1].replace( runescape, funescape );
-
- // Move the given value to match[3] whether quoted or unquoted
- match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
-
- if ( match[2] === "~=" ) {
- match[3] = " " + match[3] + " ";
- }
-
- return match.slice( 0, 4 );
- },
-
- "CHILD": function( match ) {
- /* matches from matchExpr["CHILD"]
- 1 type (only|nth|...)
- 2 what (child|of-type)
- 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
- 4 xn-component of xn+y argument ([+-]?\d*n|)
- 5 sign of xn-component
- 6 x of xn-component
- 7 sign of y-component
- 8 y of y-component
- */
- match[1] = match[1].toLowerCase();
-
- if ( match[1].slice( 0, 3 ) === "nth" ) {
- // nth-* requires argument
- if ( !match[3] ) {
- Sizzle.error( match[0] );
- }
-
- // numeric x and y parameters for Expr.filter.CHILD
- // remember that false/true cast respectively to 0/1
- match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
- match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
-
- // other types prohibit arguments
- } else if ( match[3] ) {
- Sizzle.error( match[0] );
- }
-
- return match;
- },
-
- "PSEUDO": function( match ) {
- var excess,
- unquoted = !match[6] && match[2];
-
- if ( matchExpr["CHILD"].test( match[0] ) ) {
- return null;
- }
-
- // Accept quoted arguments as-is
- if ( match[3] ) {
- match[2] = match[4] || match[5] || "";
-
- // Strip excess characters from unquoted arguments
- } else if ( unquoted && rpseudo.test( unquoted ) &&
- // Get excess from tokenize (recursively)
- (excess = tokenize( unquoted, true )) &&
- // advance to the next closing parenthesis
- (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
-
- // excess is a negative index
- match[0] = match[0].slice( 0, excess );
- match[2] = unquoted.slice( 0, excess );
- }
-
- // Return only captures needed by the pseudo filter method (type and argument)
- return match.slice( 0, 3 );
- }
- },
-
- filter: {
-
- "TAG": function( nodeNameSelector ) {
- var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
- return nodeNameSelector === "*" ?
- function() { return true; } :
- function( elem ) {
- return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
- };
- },
-
- "CLASS": function( className ) {
- var pattern = classCache[ className + " " ];
-
- return pattern ||
- (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
- classCache( className, function( elem ) {
- return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
- });
- },
-
- "ATTR": function( name, operator, check ) {
- return function( elem ) {
- var result = Sizzle.attr( elem, name );
-
- if ( result == null ) {
- return operator === "!=";
- }
- if ( !operator ) {
- return true;
- }
-
- result += "";
-
- return operator === "=" ? result === check :
- operator === "!=" ? result !== check :
- operator === "^=" ? check && result.indexOf( check ) === 0 :
- operator === "*=" ? check && result.indexOf( check ) > -1 :
- operator === "$=" ? check && result.slice( -check.length ) === check :
- operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
- operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
- false;
- };
- },
-
- "CHILD": function( type, what, argument, first, last ) {
- var simple = type.slice( 0, 3 ) !== "nth",
- forward = type.slice( -4 ) !== "last",
- ofType = what === "of-type";
-
- return first === 1 && last === 0 ?
-
- // Shortcut for :nth-*(n)
- function( elem ) {
- return !!elem.parentNode;
- } :
-
- function( elem, context, xml ) {
- var cache, outerCache, node, diff, nodeIndex, start,
- dir = simple !== forward ? "nextSibling" : "previousSibling",
- parent = elem.parentNode,
- name = ofType && elem.nodeName.toLowerCase(),
- useCache = !xml && !ofType;
-
- if ( parent ) {
-
- // :(first|last|only)-(child|of-type)
- if ( simple ) {
- while ( dir ) {
- node = elem;
- while ( (node = node[ dir ]) ) {
- if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
- return false;
- }
- }
- // Reverse direction for :only-* (if we haven't yet done so)
- start = dir = type === "only" && !start && "nextSibling";
- }
- return true;
- }
-
- start = [ forward ? parent.firstChild : parent.lastChild ];
-
- // non-xml :nth-child(...) stores cache data on `parent`
- if ( forward && useCache ) {
- // Seek `elem` from a previously-cached index
- outerCache = parent[ expando ] || (parent[ expando ] = {});
- cache = outerCache[ type ] || [];
- nodeIndex = cache[0] === dirruns && cache[1];
- diff = cache[0] === dirruns && cache[2];
- node = nodeIndex && parent.childNodes[ nodeIndex ];
-
- while ( (node = ++nodeIndex && node && node[ dir ] ||
-
- // Fallback to seeking `elem` from the start
- (diff = nodeIndex = 0) || start.pop()) ) {
-
- // When found, cache indexes on `parent` and break
- if ( node.nodeType === 1 && ++diff && node === elem ) {
- outerCache[ type ] = [ dirruns, nodeIndex, diff ];
- break;
- }
- }
-
- // Use previously-cached element index if available
- } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
- diff = cache[1];
-
- // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
- } else {
- // Use the same loop as above to seek `elem` from the start
- while ( (node = ++nodeIndex && node && node[ dir ] ||
- (diff = nodeIndex = 0) || start.pop()) ) {
-
- if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
- // Cache the index of each encountered element
- if ( useCache ) {
- (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
- }
-
- if ( node === elem ) {
- break;
- }
- }
- }
- }
-
- // Incorporate the offset, then check against cycle size
- diff -= last;
- return diff === first || ( diff % first === 0 && diff / first >= 0 );
- }
- };
- },
-
- "PSEUDO": function( pseudo, argument ) {
- // pseudo-class names are case-insensitive
- // http://www.w3.org/TR/selectors/#pseudo-classes
- // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
- // Remember that setFilters inherits from pseudos
- var args,
- fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
- Sizzle.error( "unsupported pseudo: " + pseudo );
-
- // The user may use createPseudo to indicate that
- // arguments are needed to create the filter function
- // just as Sizzle does
- if ( fn[ expando ] ) {
- return fn( argument );
- }
-
- // But maintain support for old signatures
- if ( fn.length > 1 ) {
- args = [ pseudo, pseudo, "", argument ];
- return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
- markFunction(function( seed, matches ) {
- var idx,
- matched = fn( seed, argument ),
- i = matched.length;
- while ( i-- ) {
- idx = indexOf( seed, matched[i] );
- seed[ idx ] = !( matches[ idx ] = matched[i] );
- }
- }) :
- function( elem ) {
- return fn( elem, 0, args );
- };
- }
-
- return fn;
- }
- },
-
- pseudos: {
- // Potentially complex pseudos
- "not": markFunction(function( selector ) {
- // Trim the selector passed to compile
- // to avoid treating leading and trailing
- // spaces as combinators
- var input = [],
- results = [],
- matcher = compile( selector.replace( rtrim, "$1" ) );
-
- return matcher[ expando ] ?
- markFunction(function( seed, matches, context, xml ) {
- var elem,
- unmatched = matcher( seed, null, xml, [] ),
- i = seed.length;
-
- // Match elements unmatched by `matcher`
- while ( i-- ) {
- if ( (elem = unmatched[i]) ) {
- seed[i] = !(matches[i] = elem);
- }
- }
- }) :
- function( elem, context, xml ) {
- input[0] = elem;
- matcher( input, null, xml, results );
- // Don't keep the element (issue #299)
- input[0] = null;
- return !results.pop();
- };
- }),
-
- "has": markFunction(function( selector ) {
- return function( elem ) {
- return Sizzle( selector, elem ).length > 0;
- };
- }),
-
- "contains": markFunction(function( text ) {
- text = text.replace( runescape, funescape );
- return function( elem ) {
- return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
- };
- }),
-
- // "Whether an element is represented by a :lang() selector
- // is based solely on the element's language value
- // being equal to the identifier C,
- // or beginning with the identifier C immediately followed by "-".
- // The matching of C against the element's language value is performed case-insensitively.
- // The identifier C does not have to be a valid language name."
- // http://www.w3.org/TR/selectors/#lang-pseudo
- "lang": markFunction( function( lang ) {
- // lang value must be a valid identifier
- if ( !ridentifier.test(lang || "") ) {
- Sizzle.error( "unsupported lang: " + lang );
- }
- lang = lang.replace( runescape, funescape ).toLowerCase();
- return function( elem ) {
- var elemLang;
- do {
- if ( (elemLang = documentIsHTML ?
- elem.lang :
- elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
-
- elemLang = elemLang.toLowerCase();
- return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
- }
- } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
- return false;
- };
- }),
-
- // Miscellaneous
- "target": function( elem ) {
- var hash = window.location && window.location.hash;
- return hash && hash.slice( 1 ) === elem.id;
- },
-
- "root": function( elem ) {
- return elem === docElem;
- },
-
- "focus": function( elem ) {
- return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
- },
-
- // Boolean properties
- "enabled": function( elem ) {
- return elem.disabled === false;
- },
-
- "disabled": function( elem ) {
- return elem.disabled === true;
- },
-
- "checked": function( elem ) {
- // In CSS3, :checked should return both checked and selected elements
- // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
- var nodeName = elem.nodeName.toLowerCase();
- return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
- },
-
- "selected": function( elem ) {
- // Accessing this property makes selected-by-default
- // options in Safari work properly
- if ( elem.parentNode ) {
- elem.parentNode.selectedIndex;
- }
-
- return elem.selected === true;
- },
-
- // Contents
- "empty": function( elem ) {
- // http://www.w3.org/TR/selectors/#empty-pseudo
- // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
- // but not by others (comment: 8; processing instruction: 7; etc.)
- // nodeType < 6 works because attributes (2) do not appear as children
- for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
- if ( elem.nodeType < 6 ) {
- return false;
- }
- }
- return true;
- },
-
- "parent": function( elem ) {
- return !Expr.pseudos["empty"]( elem );
- },
-
- // Element/input types
- "header": function( elem ) {
- return rheader.test( elem.nodeName );
- },
-
- "input": function( elem ) {
- return rinputs.test( elem.nodeName );
- },
-
- "button": function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return name === "input" && elem.type === "button" || name === "button";
- },
-
- "text": function( elem ) {
- var attr;
- return elem.nodeName.toLowerCase() === "input" &&
- elem.type === "text" &&
-
- // Support: IE<8
- // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
- ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
- },
-
- // Position-in-collection
- "first": createPositionalPseudo(function() {
- return [ 0 ];
- }),
-
- "last": createPositionalPseudo(function( matchIndexes, length ) {
- return [ length - 1 ];
- }),
-
- "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
- return [ argument < 0 ? argument + length : argument ];
- }),
-
- "even": createPositionalPseudo(function( matchIndexes, length ) {
- var i = 0;
- for ( ; i < length; i += 2 ) {
- matchIndexes.push( i );
- }
- return matchIndexes;
- }),
-
- "odd": createPositionalPseudo(function( matchIndexes, length ) {
- var i = 1;
- for ( ; i < length; i += 2 ) {
- matchIndexes.push( i );
- }
- return matchIndexes;
- }),
-
- "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
- var i = argument < 0 ? argument + length : argument;
- for ( ; --i >= 0; ) {
- matchIndexes.push( i );
- }
- return matchIndexes;
- }),
-
- "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
- var i = argument < 0 ? argument + length : argument;
- for ( ; ++i < length; ) {
- matchIndexes.push( i );
- }
- return matchIndexes;
- })
- }
-};
-
-Expr.pseudos["nth"] = Expr.pseudos["eq"];
-
-// Add button/input type pseudos
-for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
- Expr.pseudos[ i ] = createInputPseudo( i );
-}
-for ( i in { submit: true, reset: true } ) {
- Expr.pseudos[ i ] = createButtonPseudo( i );
-}
-
-// Easy API for creating new setFilters
-function setFilters() {}
-setFilters.prototype = Expr.filters = Expr.pseudos;
-Expr.setFilters = new setFilters();
-
-tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
- var matched, match, tokens, type,
- soFar, groups, preFilters,
- cached = tokenCache[ selector + " " ];
-
- if ( cached ) {
- return parseOnly ? 0 : cached.slice( 0 );
- }
-
- soFar = selector;
- groups = [];
- preFilters = Expr.preFilter;
-
- while ( soFar ) {
-
- // Comma and first run
- if ( !matched || (match = rcomma.exec( soFar )) ) {
- if ( match ) {
- // Don't consume trailing commas as valid
- soFar = soFar.slice( match[0].length ) || soFar;
- }
- groups.push( (tokens = []) );
- }
-
- matched = false;
-
- // Combinators
- if ( (match = rcombinators.exec( soFar )) ) {
- matched = match.shift();
- tokens.push({
- value: matched,
- // Cast descendant combinators to space
- type: match[0].replace( rtrim, " " )
- });
- soFar = soFar.slice( matched.length );
- }
-
- // Filters
- for ( type in Expr.filter ) {
- if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
- (match = preFilters[ type ]( match ))) ) {
- matched = match.shift();
- tokens.push({
- value: matched,
- type: type,
- matches: match
- });
- soFar = soFar.slice( matched.length );
- }
- }
-
- if ( !matched ) {
- break;
- }
- }
-
- // Return the length of the invalid excess
- // if we're just parsing
- // Otherwise, throw an error or return tokens
- return parseOnly ?
- soFar.length :
- soFar ?
- Sizzle.error( selector ) :
- // Cache the tokens
- tokenCache( selector, groups ).slice( 0 );
-};
-
-function toSelector( tokens ) {
- var i = 0,
- len = tokens.length,
- selector = "";
- for ( ; i < len; i++ ) {
- selector += tokens[i].value;
- }
- return selector;
-}
-
-function addCombinator( matcher, combinator, base ) {
- var dir = combinator.dir,
- checkNonElements = base && dir === "parentNode",
- doneName = done++;
-
- return combinator.first ?
- // Check against closest ancestor/preceding element
- function( elem, context, xml ) {
- while ( (elem = elem[ dir ]) ) {
- if ( elem.nodeType === 1 || checkNonElements ) {
- return matcher( elem, context, xml );
- }
- }
- } :
-
- // Check against all ancestor/preceding elements
- function( elem, context, xml ) {
- var oldCache, outerCache,
- newCache = [ dirruns, doneName ];
-
- // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
- if ( xml ) {
- while ( (elem = elem[ dir ]) ) {
- if ( elem.nodeType === 1 || checkNonElements ) {
- if ( matcher( elem, context, xml ) ) {
- return true;
- }
- }
- }
- } else {
- while ( (elem = elem[ dir ]) ) {
- if ( elem.nodeType === 1 || checkNonElements ) {
- outerCache = elem[ expando ] || (elem[ expando ] = {});
- if ( (oldCache = outerCache[ dir ]) &&
- oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
-
- // Assign to newCache so results back-propagate to previous elements
- return (newCache[ 2 ] = oldCache[ 2 ]);
- } else {
- // Reuse newcache so results back-propagate to previous elements
- outerCache[ dir ] = newCache;
-
- // A match means we're done; a fail means we have to keep checking
- if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
- return true;
- }
- }
- }
- }
- }
- };
-}
-
-function elementMatcher( matchers ) {
- return matchers.length > 1 ?
- function( elem, context, xml ) {
- var i = matchers.length;
- while ( i-- ) {
- if ( !matchers[i]( elem, context, xml ) ) {
- return false;
- }
- }
- return true;
- } :
- matchers[0];
-}
-
-function multipleContexts( selector, contexts, results ) {
- var i = 0,
- len = contexts.length;
- for ( ; i < len; i++ ) {
- Sizzle( selector, contexts[i], results );
- }
- return results;
-}
-
-function condense( unmatched, map, filter, context, xml ) {
- var elem,
- newUnmatched = [],
- i = 0,
- len = unmatched.length,
- mapped = map != null;
-
- for ( ; i < len; i++ ) {
- if ( (elem = unmatched[i]) ) {
- if ( !filter || filter( elem, context, xml ) ) {
- newUnmatched.push( elem );
- if ( mapped ) {
- map.push( i );
- }
- }
- }
- }
-
- return newUnmatched;
-}
-
-function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
- if ( postFilter && !postFilter[ expando ] ) {
- postFilter = setMatcher( postFilter );
- }
- if ( postFinder && !postFinder[ expando ] ) {
- postFinder = setMatcher( postFinder, postSelector );
- }
- return markFunction(function( seed, results, context, xml ) {
- var temp, i, elem,
- preMap = [],
- postMap = [],
- preexisting = results.length,
-
- // Get initial elements from seed or context
- elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
-
- // Prefilter to get matcher input, preserving a map for seed-results synchronization
- matcherIn = preFilter && ( seed || !selector ) ?
- condense( elems, preMap, preFilter, context, xml ) :
- elems,
-
- matcherOut = matcher ?
- // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
- postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
-
- // ...intermediate processing is necessary
- [] :
-
- // ...otherwise use results directly
- results :
- matcherIn;
-
- // Find primary matches
- if ( matcher ) {
- matcher( matcherIn, matcherOut, context, xml );
- }
-
- // Apply postFilter
- if ( postFilter ) {
- temp = condense( matcherOut, postMap );
- postFilter( temp, [], context, xml );
-
- // Un-match failing elements by moving them back to matcherIn
- i = temp.length;
- while ( i-- ) {
- if ( (elem = temp[i]) ) {
- matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
- }
- }
- }
-
- if ( seed ) {
- if ( postFinder || preFilter ) {
- if ( postFinder ) {
- // Get the final matcherOut by condensing this intermediate into postFinder contexts
- temp = [];
- i = matcherOut.length;
- while ( i-- ) {
- if ( (elem = matcherOut[i]) ) {
- // Restore matcherIn since elem is not yet a final match
- temp.push( (matcherIn[i] = elem) );
- }
- }
- postFinder( null, (matcherOut = []), temp, xml );
- }
-
- // Move matched elements from seed to results to keep them synchronized
- i = matcherOut.length;
- while ( i-- ) {
- if ( (elem = matcherOut[i]) &&
- (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
-
- seed[temp] = !(results[temp] = elem);
- }
- }
- }
-
- // Add elements to results, through postFinder if defined
- } else {
- matcherOut = condense(
- matcherOut === results ?
- matcherOut.splice( preexisting, matcherOut.length ) :
- matcherOut
- );
- if ( postFinder ) {
- postFinder( null, results, matcherOut, xml );
- } else {
- push.apply( results, matcherOut );
- }
- }
- });
-}
-
-function matcherFromTokens( tokens ) {
- var checkContext, matcher, j,
- len = tokens.length,
- leadingRelative = Expr.relative[ tokens[0].type ],
- implicitRelative = leadingRelative || Expr.relative[" "],
- i = leadingRelative ? 1 : 0,
-
- // The foundational matcher ensures that elements are reachable from top-level context(s)
- matchContext = addCombinator( function( elem ) {
- return elem === checkContext;
- }, implicitRelative, true ),
- matchAnyContext = addCombinator( function( elem ) {
- return indexOf( checkContext, elem ) > -1;
- }, implicitRelative, true ),
- matchers = [ function( elem, context, xml ) {
- var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
- (checkContext = context).nodeType ?
- matchContext( elem, context, xml ) :
- matchAnyContext( elem, context, xml ) );
- // Avoid hanging onto element (issue #299)
- checkContext = null;
- return ret;
- } ];
-
- for ( ; i < len; i++ ) {
- if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
- matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
- } else {
- matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
-
- // Return special upon seeing a positional matcher
- if ( matcher[ expando ] ) {
- // Find the next relative operator (if any) for proper handling
- j = ++i;
- for ( ; j < len; j++ ) {
- if ( Expr.relative[ tokens[j].type ] ) {
- break;
- }
- }
- return setMatcher(
- i > 1 && elementMatcher( matchers ),
- i > 1 && toSelector(
- // If the preceding token was a descendant combinator, insert an implicit any-element `*`
- tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
- ).replace( rtrim, "$1" ),
- matcher,
- i < j && matcherFromTokens( tokens.slice( i, j ) ),
- j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
- j < len && toSelector( tokens )
- );
- }
- matchers.push( matcher );
- }
- }
-
- return elementMatcher( matchers );
-}
-
-function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
- var bySet = setMatchers.length > 0,
- byElement = elementMatchers.length > 0,
- superMatcher = function( seed, context, xml, results, outermost ) {
- var elem, j, matcher,
- matchedCount = 0,
- i = "0",
- unmatched = seed && [],
- setMatched = [],
- contextBackup = outermostContext,
- // We must always have either seed elements or outermost context
- elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
- // Use integer dirruns iff this is the outermost matcher
- dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
- len = elems.length;
-
- if ( outermost ) {
- outermostContext = context !== document && context;
- }
-
- // Add elements passing elementMatchers directly to results
- // Keep `i` a string if there are no elements so `matchedCount` will be "00" below
- // Support: IE<9, Safari
- // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id
- for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
- if ( byElement && elem ) {
- j = 0;
- while ( (matcher = elementMatchers[j++]) ) {
- if ( matcher( elem, context, xml ) ) {
- results.push( elem );
- break;
- }
- }
- if ( outermost ) {
- dirruns = dirrunsUnique;
- }
- }
-
- // Track unmatched elements for set filters
- if ( bySet ) {
- // They will have gone through all possible matchers
- if ( (elem = !matcher && elem) ) {
- matchedCount--;
- }
-
- // Lengthen the array for every element, matched or not
- if ( seed ) {
- unmatched.push( elem );
- }
- }
- }
-
- // Apply set filters to unmatched elements
- matchedCount += i;
- if ( bySet && i !== matchedCount ) {
- j = 0;
- while ( (matcher = setMatchers[j++]) ) {
- matcher( unmatched, setMatched, context, xml );
- }
-
- if ( seed ) {
- // Reintegrate element matches to eliminate the need for sorting
- if ( matchedCount > 0 ) {
- while ( i-- ) {
- if ( !(unmatched[i] || setMatched[i]) ) {
- setMatched[i] = pop.call( results );
- }
- }
- }
-
- // Discard index placeholder values to get only actual matches
- setMatched = condense( setMatched );
- }
-
- // Add matches to results
- push.apply( results, setMatched );
-
- // Seedless set matches succeeding multiple successful matchers stipulate sorting
- if ( outermost && !seed && setMatched.length > 0 &&
- ( matchedCount + setMatchers.length ) > 1 ) {
-
- Sizzle.uniqueSort( results );
- }
- }
-
- // Override manipulation of globals by nested matchers
- if ( outermost ) {
- dirruns = dirrunsUnique;
- outermostContext = contextBackup;
- }
-
- return unmatched;
- };
-
- return bySet ?
- markFunction( superMatcher ) :
- superMatcher;
-}
-
-compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
- var i,
- setMatchers = [],
- elementMatchers = [],
- cached = compilerCache[ selector + " " ];
-
- if ( !cached ) {
- // Generate a function of recursive functions that can be used to check each element
- if ( !match ) {
- match = tokenize( selector );
- }
- i = match.length;
- while ( i-- ) {
- cached = matcherFromTokens( match[i] );
- if ( cached[ expando ] ) {
- setMatchers.push( cached );
- } else {
- elementMatchers.push( cached );
- }
- }
-
- // Cache the compiled function
- cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
-
- // Save selector and tokenization
- cached.selector = selector;
- }
- return cached;
-};
-
-/**
- * A low-level selection function that works with Sizzle's compiled
- * selector functions
- * @param {String|Function} selector A selector or a pre-compiled
- * selector function built with Sizzle.compile
- * @param {Element} context
- * @param {Array} [results]
- * @param {Array} [seed] A set of elements to match against
- */
-select = Sizzle.select = function( selector, context, results, seed ) {
- var i, tokens, token, type, find,
- compiled = typeof selector === "function" && selector,
- match = !seed && tokenize( (selector = compiled.selector || selector) );
-
- results = results || [];
-
- // Try to minimize operations if there is no seed and only one group
- if ( match.length === 1 ) {
-
- // Take a shortcut and set the context if the root selector is an ID
- tokens = match[0] = match[0].slice( 0 );
- if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
- support.getById && context.nodeType === 9 && documentIsHTML &&
- Expr.relative[ tokens[1].type ] ) {
-
- context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
- if ( !context ) {
- return results;
-
- // Precompiled matchers will still verify ancestry, so step up a level
- } else if ( compiled ) {
- context = context.parentNode;
- }
-
- selector = selector.slice( tokens.shift().value.length );
- }
-
- // Fetch a seed set for right-to-left matching
- i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
- while ( i-- ) {
- token = tokens[i];
-
- // Abort if we hit a combinator
- if ( Expr.relative[ (type = token.type) ] ) {
- break;
- }
- if ( (find = Expr.find[ type ]) ) {
- // Search, expanding context for leading sibling combinators
- if ( (seed = find(
- token.matches[0].replace( runescape, funescape ),
- rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
- )) ) {
-
- // If seed is empty or no tokens remain, we can return early
- tokens.splice( i, 1 );
- selector = seed.length && toSelector( tokens );
- if ( !selector ) {
- push.apply( results, seed );
- return results;
- }
-
- break;
- }
- }
- }
- }
-
- // Compile and execute a filtering function if one is not provided
- // Provide `match` to avoid retokenization if we modified the selector above
- ( compiled || compile( selector, match ) )(
- seed,
- context,
- !documentIsHTML,
- results,
- rsibling.test( selector ) && testContext( context.parentNode ) || context
- );
- return results;
-};
-
-// One-time assignments
-
-// Sort stability
-support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
-
-// Support: Chrome 14-35+
-// Always assume duplicates if they aren't passed to the comparison function
-support.detectDuplicates = !!hasDuplicate;
-
-// Initialize against the default document
-setDocument();
-
-// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
-// Detached nodes confoundingly follow *each other*
-support.sortDetached = assert(function( div1 ) {
- // Should return 1, but returns 4 (following)
- return div1.compareDocumentPosition( document.createElement("div") ) & 1;
-});
-
-// Support: IE<8
-// Prevent attribute/property "interpolation"
-// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
-if ( !assert(function( div ) {
- div.innerHTML = " ";
- return div.firstChild.getAttribute("href") === "#" ;
-}) ) {
- addHandle( "type|href|height|width", function( elem, name, isXML ) {
- if ( !isXML ) {
- return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
- }
- });
-}
-
-// Support: IE<9
-// Use defaultValue in place of getAttribute("value")
-if ( !support.attributes || !assert(function( div ) {
- div.innerHTML = " ";
- div.firstChild.setAttribute( "value", "" );
- return div.firstChild.getAttribute( "value" ) === "";
-}) ) {
- addHandle( "value", function( elem, name, isXML ) {
- if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
- return elem.defaultValue;
- }
- });
-}
-
-// Support: IE<9
-// Use getAttributeNode to fetch booleans when getAttribute lies
-if ( !assert(function( div ) {
- return div.getAttribute("disabled") == null;
-}) ) {
- addHandle( booleans, function( elem, name, isXML ) {
- var val;
- if ( !isXML ) {
- return elem[ name ] === true ? name.toLowerCase() :
- (val = elem.getAttributeNode( name )) && val.specified ?
- val.value :
- null;
- }
- });
-}
-
-return Sizzle;
-
-})( window );
-
-
-
-jQuery.find = Sizzle;
-jQuery.expr = Sizzle.selectors;
-jQuery.expr[":"] = jQuery.expr.pseudos;
-jQuery.unique = Sizzle.uniqueSort;
-jQuery.text = Sizzle.getText;
-jQuery.isXMLDoc = Sizzle.isXML;
-jQuery.contains = Sizzle.contains;
-
-
-
-var rneedsContext = jQuery.expr.match.needsContext;
-
-var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/);
-
-
-
-var risSimple = /^.[^:#\[\.,]*$/;
-
-// Implement the identical functionality for filter and not
-function winnow( elements, qualifier, not ) {
- if ( jQuery.isFunction( qualifier ) ) {
- return jQuery.grep( elements, function( elem, i ) {
- /* jshint -W018 */
- return !!qualifier.call( elem, i, elem ) !== not;
- });
-
- }
-
- if ( qualifier.nodeType ) {
- return jQuery.grep( elements, function( elem ) {
- return ( elem === qualifier ) !== not;
- });
-
- }
-
- if ( typeof qualifier === "string" ) {
- if ( risSimple.test( qualifier ) ) {
- return jQuery.filter( qualifier, elements, not );
- }
-
- qualifier = jQuery.filter( qualifier, elements );
- }
-
- return jQuery.grep( elements, function( elem ) {
- return ( indexOf.call( qualifier, elem ) >= 0 ) !== not;
- });
-}
-
-jQuery.filter = function( expr, elems, not ) {
- var elem = elems[ 0 ];
-
- if ( not ) {
- expr = ":not(" + expr + ")";
- }
-
- return elems.length === 1 && elem.nodeType === 1 ?
- jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
- jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
- return elem.nodeType === 1;
- }));
-};
-
-jQuery.fn.extend({
- find: function( selector ) {
- var i,
- len = this.length,
- ret = [],
- self = this;
-
- if ( typeof selector !== "string" ) {
- return this.pushStack( jQuery( selector ).filter(function() {
- for ( i = 0; i < len; i++ ) {
- if ( jQuery.contains( self[ i ], this ) ) {
- return true;
- }
- }
- }) );
- }
-
- for ( i = 0; i < len; i++ ) {
- jQuery.find( selector, self[ i ], ret );
- }
-
- // Needed because $( selector, context ) becomes $( context ).find( selector )
- ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
- ret.selector = this.selector ? this.selector + " " + selector : selector;
- return ret;
- },
- filter: function( selector ) {
- return this.pushStack( winnow(this, selector || [], false) );
- },
- not: function( selector ) {
- return this.pushStack( winnow(this, selector || [], true) );
- },
- is: function( selector ) {
- return !!winnow(
- this,
-
- // If this is a positional/relative selector, check membership in the returned set
- // so $("p:first").is("p:last") won't return true for a doc with two "p".
- typeof selector === "string" && rneedsContext.test( selector ) ?
- jQuery( selector ) :
- selector || [],
- false
- ).length;
- }
-});
-
-
-// Initialize a jQuery object
-
-
-// A central reference to the root jQuery(document)
-var rootjQuery,
-
- // A simple way to check for HTML strings
- // Prioritize #id over to avoid XSS via location.hash (#9521)
- // Strict HTML recognition (#11290: must start with <)
- rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
-
- init = jQuery.fn.init = function( selector, context ) {
- var match, elem;
-
- // HANDLE: $(""), $(null), $(undefined), $(false)
- if ( !selector ) {
- return this;
- }
-
- // Handle HTML strings
- if ( typeof selector === "string" ) {
- if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) {
- // Assume that strings that start and end with <> are HTML and skip the regex check
- match = [ null, selector, null ];
-
- } else {
- match = rquickExpr.exec( selector );
- }
-
- // Match html or make sure no context is specified for #id
- if ( match && (match[1] || !context) ) {
-
- // HANDLE: $(html) -> $(array)
- if ( match[1] ) {
- context = context instanceof jQuery ? context[0] : context;
-
- // Option to run scripts is true for back-compat
- // Intentionally let the error be thrown if parseHTML is not present
- jQuery.merge( this, jQuery.parseHTML(
- match[1],
- context && context.nodeType ? context.ownerDocument || context : document,
- true
- ) );
-
- // HANDLE: $(html, props)
- if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
- for ( match in context ) {
- // Properties of context are called as methods if possible
- if ( jQuery.isFunction( this[ match ] ) ) {
- this[ match ]( context[ match ] );
-
- // ...and otherwise set as attributes
- } else {
- this.attr( match, context[ match ] );
- }
- }
- }
-
- return this;
-
- // HANDLE: $(#id)
- } else {
- elem = document.getElementById( match[2] );
-
- // Support: Blackberry 4.6
- // gEBID returns nodes no longer in the document (#6963)
- if ( elem && elem.parentNode ) {
- // Inject the element directly into the jQuery object
- this.length = 1;
- this[0] = elem;
- }
-
- this.context = document;
- this.selector = selector;
- return this;
- }
-
- // HANDLE: $(expr, $(...))
- } else if ( !context || context.jquery ) {
- return ( context || rootjQuery ).find( selector );
-
- // HANDLE: $(expr, context)
- // (which is just equivalent to: $(context).find(expr)
- } else {
- return this.constructor( context ).find( selector );
- }
-
- // HANDLE: $(DOMElement)
- } else if ( selector.nodeType ) {
- this.context = this[0] = selector;
- this.length = 1;
- return this;
-
- // HANDLE: $(function)
- // Shortcut for document ready
- } else if ( jQuery.isFunction( selector ) ) {
- return typeof rootjQuery.ready !== "undefined" ?
- rootjQuery.ready( selector ) :
- // Execute immediately if ready is not present
- selector( jQuery );
- }
-
- if ( selector.selector !== undefined ) {
- this.selector = selector.selector;
- this.context = selector.context;
- }
-
- return jQuery.makeArray( selector, this );
- };
-
-// Give the init function the jQuery prototype for later instantiation
-init.prototype = jQuery.fn;
-
-// Initialize central reference
-rootjQuery = jQuery( document );
-
-
-var rparentsprev = /^(?:parents|prev(?:Until|All))/,
- // Methods guaranteed to produce a unique set when starting from a unique set
- guaranteedUnique = {
- children: true,
- contents: true,
- next: true,
- prev: true
- };
-
-jQuery.extend({
- dir: function( elem, dir, until ) {
- var matched = [],
- truncate = until !== undefined;
-
- while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) {
- if ( elem.nodeType === 1 ) {
- if ( truncate && jQuery( elem ).is( until ) ) {
- break;
- }
- matched.push( elem );
- }
- }
- return matched;
- },
-
- sibling: function( n, elem ) {
- var matched = [];
-
- for ( ; n; n = n.nextSibling ) {
- if ( n.nodeType === 1 && n !== elem ) {
- matched.push( n );
- }
- }
-
- return matched;
- }
-});
-
-jQuery.fn.extend({
- has: function( target ) {
- var targets = jQuery( target, this ),
- l = targets.length;
-
- return this.filter(function() {
- var i = 0;
- for ( ; i < l; i++ ) {
- if ( jQuery.contains( this, targets[i] ) ) {
- return true;
- }
- }
- });
- },
-
- closest: function( selectors, context ) {
- var cur,
- i = 0,
- l = this.length,
- matched = [],
- pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
- jQuery( selectors, context || this.context ) :
- 0;
-
- for ( ; i < l; i++ ) {
- for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {
- // Always skip document fragments
- if ( cur.nodeType < 11 && (pos ?
- pos.index(cur) > -1 :
-
- // Don't pass non-elements to Sizzle
- cur.nodeType === 1 &&
- jQuery.find.matchesSelector(cur, selectors)) ) {
-
- matched.push( cur );
- break;
- }
- }
- }
-
- return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched );
- },
-
- // Determine the position of an element within the set
- index: function( elem ) {
-
- // No argument, return index in parent
- if ( !elem ) {
- return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
- }
-
- // Index in selector
- if ( typeof elem === "string" ) {
- return indexOf.call( jQuery( elem ), this[ 0 ] );
- }
-
- // Locate the position of the desired element
- return indexOf.call( this,
-
- // If it receives a jQuery object, the first element is used
- elem.jquery ? elem[ 0 ] : elem
- );
- },
-
- add: function( selector, context ) {
- return this.pushStack(
- jQuery.unique(
- jQuery.merge( this.get(), jQuery( selector, context ) )
- )
- );
- },
-
- addBack: function( selector ) {
- return this.add( selector == null ?
- this.prevObject : this.prevObject.filter(selector)
- );
- }
-});
-
-function sibling( cur, dir ) {
- while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {}
- return cur;
-}
-
-jQuery.each({
- parent: function( elem ) {
- var parent = elem.parentNode;
- return parent && parent.nodeType !== 11 ? parent : null;
- },
- parents: function( elem ) {
- return jQuery.dir( elem, "parentNode" );
- },
- parentsUntil: function( elem, i, until ) {
- return jQuery.dir( elem, "parentNode", until );
- },
- next: function( elem ) {
- return sibling( elem, "nextSibling" );
- },
- prev: function( elem ) {
- return sibling( elem, "previousSibling" );
- },
- nextAll: function( elem ) {
- return jQuery.dir( elem, "nextSibling" );
- },
- prevAll: function( elem ) {
- return jQuery.dir( elem, "previousSibling" );
- },
- nextUntil: function( elem, i, until ) {
- return jQuery.dir( elem, "nextSibling", until );
- },
- prevUntil: function( elem, i, until ) {
- return jQuery.dir( elem, "previousSibling", until );
- },
- siblings: function( elem ) {
- return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
- },
- children: function( elem ) {
- return jQuery.sibling( elem.firstChild );
- },
- contents: function( elem ) {
- return elem.contentDocument || jQuery.merge( [], elem.childNodes );
- }
-}, function( name, fn ) {
- jQuery.fn[ name ] = function( until, selector ) {
- var matched = jQuery.map( this, fn, until );
-
- if ( name.slice( -5 ) !== "Until" ) {
- selector = until;
- }
-
- if ( selector && typeof selector === "string" ) {
- matched = jQuery.filter( selector, matched );
- }
-
- if ( this.length > 1 ) {
- // Remove duplicates
- if ( !guaranteedUnique[ name ] ) {
- jQuery.unique( matched );
- }
-
- // Reverse order for parents* and prev-derivatives
- if ( rparentsprev.test( name ) ) {
- matched.reverse();
- }
- }
-
- return this.pushStack( matched );
- };
-});
-var rnotwhite = (/\S+/g);
-
-
-
-// String to Object options format cache
-var optionsCache = {};
-
-// Convert String-formatted options into Object-formatted ones and store in cache
-function createOptions( options ) {
- var object = optionsCache[ options ] = {};
- jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
- object[ flag ] = true;
- });
- return object;
-}
-
-/*
- * Create a callback list using the following parameters:
- *
- * options: an optional list of space-separated options that will change how
- * the callback list behaves or a more traditional option object
- *
- * By default a callback list will act like an event callback list and can be
- * "fired" multiple times.
- *
- * Possible options:
- *
- * once: will ensure the callback list can only be fired once (like a Deferred)
- *
- * memory: will keep track of previous values and will call any callback added
- * after the list has been fired right away with the latest "memorized"
- * values (like a Deferred)
- *
- * unique: will ensure a callback can only be added once (no duplicate in the list)
- *
- * stopOnFalse: interrupt callings when a callback returns false
- *
- */
-jQuery.Callbacks = function( options ) {
-
- // Convert options from String-formatted to Object-formatted if needed
- // (we check in cache first)
- options = typeof options === "string" ?
- ( optionsCache[ options ] || createOptions( options ) ) :
- jQuery.extend( {}, options );
-
- var // Last fire value (for non-forgettable lists)
- memory,
- // Flag to know if list was already fired
- fired,
- // Flag to know if list is currently firing
- firing,
- // First callback to fire (used internally by add and fireWith)
- firingStart,
- // End of the loop when firing
- firingLength,
- // Index of currently firing callback (modified by remove if needed)
- firingIndex,
- // Actual callback list
- list = [],
- // Stack of fire calls for repeatable lists
- stack = !options.once && [],
- // Fire callbacks
- fire = function( data ) {
- memory = options.memory && data;
- fired = true;
- firingIndex = firingStart || 0;
- firingStart = 0;
- firingLength = list.length;
- firing = true;
- for ( ; list && firingIndex < firingLength; firingIndex++ ) {
- if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
- memory = false; // To prevent further calls using add
- break;
- }
- }
- firing = false;
- if ( list ) {
- if ( stack ) {
- if ( stack.length ) {
- fire( stack.shift() );
- }
- } else if ( memory ) {
- list = [];
- } else {
- self.disable();
- }
- }
- },
- // Actual Callbacks object
- self = {
- // Add a callback or a collection of callbacks to the list
- add: function() {
- if ( list ) {
- // First, we save the current length
- var start = list.length;
- (function add( args ) {
- jQuery.each( args, function( _, arg ) {
- var type = jQuery.type( arg );
- if ( type === "function" ) {
- if ( !options.unique || !self.has( arg ) ) {
- list.push( arg );
- }
- } else if ( arg && arg.length && type !== "string" ) {
- // Inspect recursively
- add( arg );
- }
- });
- })( arguments );
- // Do we need to add the callbacks to the
- // current firing batch?
- if ( firing ) {
- firingLength = list.length;
- // With memory, if we're not firing then
- // we should call right away
- } else if ( memory ) {
- firingStart = start;
- fire( memory );
- }
- }
- return this;
- },
- // Remove a callback from the list
- remove: function() {
- if ( list ) {
- jQuery.each( arguments, function( _, arg ) {
- var index;
- while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
- list.splice( index, 1 );
- // Handle firing indexes
- if ( firing ) {
- if ( index <= firingLength ) {
- firingLength--;
- }
- if ( index <= firingIndex ) {
- firingIndex--;
- }
- }
- }
- });
- }
- return this;
- },
- // Check if a given callback is in the list.
- // If no argument is given, return whether or not list has callbacks attached.
- has: function( fn ) {
- return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
- },
- // Remove all callbacks from the list
- empty: function() {
- list = [];
- firingLength = 0;
- return this;
- },
- // Have the list do nothing anymore
- disable: function() {
- list = stack = memory = undefined;
- return this;
- },
- // Is it disabled?
- disabled: function() {
- return !list;
- },
- // Lock the list in its current state
- lock: function() {
- stack = undefined;
- if ( !memory ) {
- self.disable();
- }
- return this;
- },
- // Is it locked?
- locked: function() {
- return !stack;
- },
- // Call all callbacks with the given context and arguments
- fireWith: function( context, args ) {
- if ( list && ( !fired || stack ) ) {
- args = args || [];
- args = [ context, args.slice ? args.slice() : args ];
- if ( firing ) {
- stack.push( args );
- } else {
- fire( args );
- }
- }
- return this;
- },
- // Call all the callbacks with the given arguments
- fire: function() {
- self.fireWith( this, arguments );
- return this;
- },
- // To know if the callbacks have already been called at least once
- fired: function() {
- return !!fired;
- }
- };
-
- return self;
-};
-
-
-jQuery.extend({
-
- Deferred: function( func ) {
- var tuples = [
- // action, add listener, listener list, final state
- [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
- [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
- [ "notify", "progress", jQuery.Callbacks("memory") ]
- ],
- state = "pending",
- promise = {
- state: function() {
- return state;
- },
- always: function() {
- deferred.done( arguments ).fail( arguments );
- return this;
- },
- then: function( /* fnDone, fnFail, fnProgress */ ) {
- var fns = arguments;
- return jQuery.Deferred(function( newDefer ) {
- jQuery.each( tuples, function( i, tuple ) {
- var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
- // deferred[ done | fail | progress ] for forwarding actions to newDefer
- deferred[ tuple[1] ](function() {
- var returned = fn && fn.apply( this, arguments );
- if ( returned && jQuery.isFunction( returned.promise ) ) {
- returned.promise()
- .done( newDefer.resolve )
- .fail( newDefer.reject )
- .progress( newDefer.notify );
- } else {
- newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
- }
- });
- });
- fns = null;
- }).promise();
- },
- // Get a promise for this deferred
- // If obj is provided, the promise aspect is added to the object
- promise: function( obj ) {
- return obj != null ? jQuery.extend( obj, promise ) : promise;
- }
- },
- deferred = {};
-
- // Keep pipe for back-compat
- promise.pipe = promise.then;
-
- // Add list-specific methods
- jQuery.each( tuples, function( i, tuple ) {
- var list = tuple[ 2 ],
- stateString = tuple[ 3 ];
-
- // promise[ done | fail | progress ] = list.add
- promise[ tuple[1] ] = list.add;
-
- // Handle state
- if ( stateString ) {
- list.add(function() {
- // state = [ resolved | rejected ]
- state = stateString;
-
- // [ reject_list | resolve_list ].disable; progress_list.lock
- }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
- }
-
- // deferred[ resolve | reject | notify ]
- deferred[ tuple[0] ] = function() {
- deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
- return this;
- };
- deferred[ tuple[0] + "With" ] = list.fireWith;
- });
-
- // Make the deferred a promise
- promise.promise( deferred );
-
- // Call given func if any
- if ( func ) {
- func.call( deferred, deferred );
- }
-
- // All done!
- return deferred;
- },
-
- // Deferred helper
- when: function( subordinate /* , ..., subordinateN */ ) {
- var i = 0,
- resolveValues = slice.call( arguments ),
- length = resolveValues.length,
-
- // the count of uncompleted subordinates
- remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
-
- // the master Deferred. If resolveValues consist of only a single Deferred, just use that.
- deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
-
- // Update function for both resolve and progress values
- updateFunc = function( i, contexts, values ) {
- return function( value ) {
- contexts[ i ] = this;
- values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
- if ( values === progressValues ) {
- deferred.notifyWith( contexts, values );
- } else if ( !( --remaining ) ) {
- deferred.resolveWith( contexts, values );
- }
- };
- },
-
- progressValues, progressContexts, resolveContexts;
-
- // Add listeners to Deferred subordinates; treat others as resolved
- if ( length > 1 ) {
- progressValues = new Array( length );
- progressContexts = new Array( length );
- resolveContexts = new Array( length );
- for ( ; i < length; i++ ) {
- if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
- resolveValues[ i ].promise()
- .done( updateFunc( i, resolveContexts, resolveValues ) )
- .fail( deferred.reject )
- .progress( updateFunc( i, progressContexts, progressValues ) );
- } else {
- --remaining;
- }
- }
- }
-
- // If we're not waiting on anything, resolve the master
- if ( !remaining ) {
- deferred.resolveWith( resolveContexts, resolveValues );
- }
-
- return deferred.promise();
- }
-});
-
-
-// The deferred used on DOM ready
-var readyList;
-
-jQuery.fn.ready = function( fn ) {
- // Add the callback
- jQuery.ready.promise().done( fn );
-
- return this;
-};
-
-jQuery.extend({
- // Is the DOM ready to be used? Set to true once it occurs.
- isReady: false,
-
- // A counter to track how many items to wait for before
- // the ready event fires. See #6781
- readyWait: 1,
-
- // Hold (or release) the ready event
- holdReady: function( hold ) {
- if ( hold ) {
- jQuery.readyWait++;
- } else {
- jQuery.ready( true );
- }
- },
-
- // Handle when the DOM is ready
- ready: function( wait ) {
-
- // Abort if there are pending holds or we're already ready
- if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
- return;
- }
-
- // Remember that the DOM is ready
- jQuery.isReady = true;
-
- // If a normal DOM Ready event fired, decrement, and wait if need be
- if ( wait !== true && --jQuery.readyWait > 0 ) {
- return;
- }
-
- // If there are functions bound, to execute
- readyList.resolveWith( document, [ jQuery ] );
-
- // Trigger any bound ready events
- if ( jQuery.fn.triggerHandler ) {
- jQuery( document ).triggerHandler( "ready" );
- jQuery( document ).off( "ready" );
- }
- }
-});
-
-/**
- * The ready event handler and self cleanup method
- */
-function completed() {
- document.removeEventListener( "DOMContentLoaded", completed, false );
- window.removeEventListener( "load", completed, false );
- jQuery.ready();
-}
-
-jQuery.ready.promise = function( obj ) {
- if ( !readyList ) {
-
- readyList = jQuery.Deferred();
-
- // Catch cases where $(document).ready() is called after the browser event has already occurred.
- // We once tried to use readyState "interactive" here, but it caused issues like the one
- // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
- if ( document.readyState === "complete" ) {
- // Handle it asynchronously to allow scripts the opportunity to delay ready
- setTimeout( jQuery.ready );
-
- } else {
-
- // Use the handy event callback
- document.addEventListener( "DOMContentLoaded", completed, false );
-
- // A fallback to window.onload, that will always work
- window.addEventListener( "load", completed, false );
- }
- }
- return readyList.promise( obj );
-};
-
-// Kick off the DOM ready check even if the user does not
-jQuery.ready.promise();
-
-
-
-
-// Multifunctional method to get and set values of a collection
-// The value/s can optionally be executed if it's a function
-var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
- var i = 0,
- len = elems.length,
- bulk = key == null;
-
- // Sets many values
- if ( jQuery.type( key ) === "object" ) {
- chainable = true;
- for ( i in key ) {
- jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
- }
-
- // Sets one value
- } else if ( value !== undefined ) {
- chainable = true;
-
- if ( !jQuery.isFunction( value ) ) {
- raw = true;
- }
-
- if ( bulk ) {
- // Bulk operations run against the entire set
- if ( raw ) {
- fn.call( elems, value );
- fn = null;
-
- // ...except when executing function values
- } else {
- bulk = fn;
- fn = function( elem, key, value ) {
- return bulk.call( jQuery( elem ), value );
- };
- }
- }
-
- if ( fn ) {
- for ( ; i < len; i++ ) {
- fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
- }
- }
- }
-
- return chainable ?
- elems :
-
- // Gets
- bulk ?
- fn.call( elems ) :
- len ? fn( elems[0], key ) : emptyGet;
-};
-
-
-/**
- * Determines whether an object can have data
- */
-jQuery.acceptData = function( owner ) {
- // Accepts only:
- // - Node
- // - Node.ELEMENT_NODE
- // - Node.DOCUMENT_NODE
- // - Object
- // - Any
- /* jshint -W018 */
- return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
-};
-
-
-function Data() {
- // Support: Android<4,
- // Old WebKit does not have Object.preventExtensions/freeze method,
- // return new empty object instead with no [[set]] accessor
- Object.defineProperty( this.cache = {}, 0, {
- get: function() {
- return {};
- }
- });
-
- this.expando = jQuery.expando + Data.uid++;
-}
-
-Data.uid = 1;
-Data.accepts = jQuery.acceptData;
-
-Data.prototype = {
- key: function( owner ) {
- // We can accept data for non-element nodes in modern browsers,
- // but we should not, see #8335.
- // Always return the key for a frozen object.
- if ( !Data.accepts( owner ) ) {
- return 0;
- }
-
- var descriptor = {},
- // Check if the owner object already has a cache key
- unlock = owner[ this.expando ];
-
- // If not, create one
- if ( !unlock ) {
- unlock = Data.uid++;
-
- // Secure it in a non-enumerable, non-writable property
- try {
- descriptor[ this.expando ] = { value: unlock };
- Object.defineProperties( owner, descriptor );
-
- // Support: Android<4
- // Fallback to a less secure definition
- } catch ( e ) {
- descriptor[ this.expando ] = unlock;
- jQuery.extend( owner, descriptor );
- }
- }
-
- // Ensure the cache object
- if ( !this.cache[ unlock ] ) {
- this.cache[ unlock ] = {};
- }
-
- return unlock;
- },
- set: function( owner, data, value ) {
- var prop,
- // There may be an unlock assigned to this node,
- // if there is no entry for this "owner", create one inline
- // and set the unlock as though an owner entry had always existed
- unlock = this.key( owner ),
- cache = this.cache[ unlock ];
-
- // Handle: [ owner, key, value ] args
- if ( typeof data === "string" ) {
- cache[ data ] = value;
-
- // Handle: [ owner, { properties } ] args
- } else {
- // Fresh assignments by object are shallow copied
- if ( jQuery.isEmptyObject( cache ) ) {
- jQuery.extend( this.cache[ unlock ], data );
- // Otherwise, copy the properties one-by-one to the cache object
- } else {
- for ( prop in data ) {
- cache[ prop ] = data[ prop ];
- }
- }
- }
- return cache;
- },
- get: function( owner, key ) {
- // Either a valid cache is found, or will be created.
- // New caches will be created and the unlock returned,
- // allowing direct access to the newly created
- // empty data object. A valid owner object must be provided.
- var cache = this.cache[ this.key( owner ) ];
-
- return key === undefined ?
- cache : cache[ key ];
- },
- access: function( owner, key, value ) {
- var stored;
- // In cases where either:
- //
- // 1. No key was specified
- // 2. A string key was specified, but no value provided
- //
- // Take the "read" path and allow the get method to determine
- // which value to return, respectively either:
- //
- // 1. The entire cache object
- // 2. The data stored at the key
- //
- if ( key === undefined ||
- ((key && typeof key === "string") && value === undefined) ) {
-
- stored = this.get( owner, key );
-
- return stored !== undefined ?
- stored : this.get( owner, jQuery.camelCase(key) );
- }
-
- // [*]When the key is not a string, or both a key and value
- // are specified, set or extend (existing objects) with either:
- //
- // 1. An object of properties
- // 2. A key and value
- //
- this.set( owner, key, value );
-
- // Since the "set" path can have two possible entry points
- // return the expected data based on which path was taken[*]
- return value !== undefined ? value : key;
- },
- remove: function( owner, key ) {
- var i, name, camel,
- unlock = this.key( owner ),
- cache = this.cache[ unlock ];
-
- if ( key === undefined ) {
- this.cache[ unlock ] = {};
-
- } else {
- // Support array or space separated string of keys
- if ( jQuery.isArray( key ) ) {
- // If "name" is an array of keys...
- // When data is initially created, via ("key", "val") signature,
- // keys will be converted to camelCase.
- // Since there is no way to tell _how_ a key was added, remove
- // both plain key and camelCase key. #12786
- // This will only penalize the array argument path.
- name = key.concat( key.map( jQuery.camelCase ) );
- } else {
- camel = jQuery.camelCase( key );
- // Try the string as a key before any manipulation
- if ( key in cache ) {
- name = [ key, camel ];
- } else {
- // If a key with the spaces exists, use it.
- // Otherwise, create an array by matching non-whitespace
- name = camel;
- name = name in cache ?
- [ name ] : ( name.match( rnotwhite ) || [] );
- }
- }
-
- i = name.length;
- while ( i-- ) {
- delete cache[ name[ i ] ];
- }
- }
- },
- hasData: function( owner ) {
- return !jQuery.isEmptyObject(
- this.cache[ owner[ this.expando ] ] || {}
- );
- },
- discard: function( owner ) {
- if ( owner[ this.expando ] ) {
- delete this.cache[ owner[ this.expando ] ];
- }
- }
-};
-var data_priv = new Data();
-
-var data_user = new Data();
-
-
-
-// Implementation Summary
-//
-// 1. Enforce API surface and semantic compatibility with 1.9.x branch
-// 2. Improve the module's maintainability by reducing the storage
-// paths to a single mechanism.
-// 3. Use the same single mechanism to support "private" and "user" data.
-// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
-// 5. Avoid exposing implementation details on user objects (eg. expando properties)
-// 6. Provide a clear path for implementation upgrade to WeakMap in 2014
-
-var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
- rmultiDash = /([A-Z])/g;
-
-function dataAttr( elem, key, data ) {
- var name;
-
- // If nothing was found internally, try to fetch any
- // data from the HTML5 data-* attribute
- if ( data === undefined && elem.nodeType === 1 ) {
- name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
- data = elem.getAttribute( name );
-
- if ( typeof data === "string" ) {
- try {
- data = data === "true" ? true :
- data === "false" ? false :
- data === "null" ? null :
- // Only convert to a number if it doesn't change the string
- +data + "" === data ? +data :
- rbrace.test( data ) ? jQuery.parseJSON( data ) :
- data;
- } catch( e ) {}
-
- // Make sure we set the data so it isn't changed later
- data_user.set( elem, key, data );
- } else {
- data = undefined;
- }
- }
- return data;
-}
-
-jQuery.extend({
- hasData: function( elem ) {
- return data_user.hasData( elem ) || data_priv.hasData( elem );
- },
-
- data: function( elem, name, data ) {
- return data_user.access( elem, name, data );
- },
-
- removeData: function( elem, name ) {
- data_user.remove( elem, name );
- },
-
- // TODO: Now that all calls to _data and _removeData have been replaced
- // with direct calls to data_priv methods, these can be deprecated.
- _data: function( elem, name, data ) {
- return data_priv.access( elem, name, data );
- },
-
- _removeData: function( elem, name ) {
- data_priv.remove( elem, name );
- }
-});
-
-jQuery.fn.extend({
- data: function( key, value ) {
- var i, name, data,
- elem = this[ 0 ],
- attrs = elem && elem.attributes;
-
- // Gets all values
- if ( key === undefined ) {
- if ( this.length ) {
- data = data_user.get( elem );
-
- if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) {
- i = attrs.length;
- while ( i-- ) {
-
- // Support: IE11+
- // The attrs elements can be null (#14894)
- if ( attrs[ i ] ) {
- name = attrs[ i ].name;
- if ( name.indexOf( "data-" ) === 0 ) {
- name = jQuery.camelCase( name.slice(5) );
- dataAttr( elem, name, data[ name ] );
- }
- }
- }
- data_priv.set( elem, "hasDataAttrs", true );
- }
- }
-
- return data;
- }
-
- // Sets multiple values
- if ( typeof key === "object" ) {
- return this.each(function() {
- data_user.set( this, key );
- });
- }
-
- return access( this, function( value ) {
- var data,
- camelKey = jQuery.camelCase( key );
-
- // The calling jQuery object (element matches) is not empty
- // (and therefore has an element appears at this[ 0 ]) and the
- // `value` parameter was not undefined. An empty jQuery object
- // will result in `undefined` for elem = this[ 0 ] which will
- // throw an exception if an attempt to read a data cache is made.
- if ( elem && value === undefined ) {
- // Attempt to get data from the cache
- // with the key as-is
- data = data_user.get( elem, key );
- if ( data !== undefined ) {
- return data;
- }
-
- // Attempt to get data from the cache
- // with the key camelized
- data = data_user.get( elem, camelKey );
- if ( data !== undefined ) {
- return data;
- }
-
- // Attempt to "discover" the data in
- // HTML5 custom data-* attrs
- data = dataAttr( elem, camelKey, undefined );
- if ( data !== undefined ) {
- return data;
- }
-
- // We tried really hard, but the data doesn't exist.
- return;
- }
-
- // Set the data...
- this.each(function() {
- // First, attempt to store a copy or reference of any
- // data that might've been store with a camelCased key.
- var data = data_user.get( this, camelKey );
-
- // For HTML5 data-* attribute interop, we have to
- // store property names with dashes in a camelCase form.
- // This might not apply to all properties...*
- data_user.set( this, camelKey, value );
-
- // *... In the case of properties that might _actually_
- // have dashes, we need to also store a copy of that
- // unchanged property.
- if ( key.indexOf("-") !== -1 && data !== undefined ) {
- data_user.set( this, key, value );
- }
- });
- }, null, value, arguments.length > 1, null, true );
- },
-
- removeData: function( key ) {
- return this.each(function() {
- data_user.remove( this, key );
- });
- }
-});
-
-
-jQuery.extend({
- queue: function( elem, type, data ) {
- var queue;
-
- if ( elem ) {
- type = ( type || "fx" ) + "queue";
- queue = data_priv.get( elem, type );
-
- // Speed up dequeue by getting out quickly if this is just a lookup
- if ( data ) {
- if ( !queue || jQuery.isArray( data ) ) {
- queue = data_priv.access( elem, type, jQuery.makeArray(data) );
- } else {
- queue.push( data );
- }
- }
- return queue || [];
- }
- },
-
- dequeue: function( elem, type ) {
- type = type || "fx";
-
- var queue = jQuery.queue( elem, type ),
- startLength = queue.length,
- fn = queue.shift(),
- hooks = jQuery._queueHooks( elem, type ),
- next = function() {
- jQuery.dequeue( elem, type );
- };
-
- // If the fx queue is dequeued, always remove the progress sentinel
- if ( fn === "inprogress" ) {
- fn = queue.shift();
- startLength--;
- }
-
- if ( fn ) {
-
- // Add a progress sentinel to prevent the fx queue from being
- // automatically dequeued
- if ( type === "fx" ) {
- queue.unshift( "inprogress" );
- }
-
- // Clear up the last queue stop function
- delete hooks.stop;
- fn.call( elem, next, hooks );
- }
-
- if ( !startLength && hooks ) {
- hooks.empty.fire();
- }
- },
-
- // Not public - generate a queueHooks object, or return the current one
- _queueHooks: function( elem, type ) {
- var key = type + "queueHooks";
- return data_priv.get( elem, key ) || data_priv.access( elem, key, {
- empty: jQuery.Callbacks("once memory").add(function() {
- data_priv.remove( elem, [ type + "queue", key ] );
- })
- });
- }
-});
-
-jQuery.fn.extend({
- queue: function( type, data ) {
- var setter = 2;
-
- if ( typeof type !== "string" ) {
- data = type;
- type = "fx";
- setter--;
- }
-
- if ( arguments.length < setter ) {
- return jQuery.queue( this[0], type );
- }
-
- return data === undefined ?
- this :
- this.each(function() {
- var queue = jQuery.queue( this, type, data );
-
- // Ensure a hooks for this queue
- jQuery._queueHooks( this, type );
-
- if ( type === "fx" && queue[0] !== "inprogress" ) {
- jQuery.dequeue( this, type );
- }
- });
- },
- dequeue: function( type ) {
- return this.each(function() {
- jQuery.dequeue( this, type );
- });
- },
- clearQueue: function( type ) {
- return this.queue( type || "fx", [] );
- },
- // Get a promise resolved when queues of a certain type
- // are emptied (fx is the type by default)
- promise: function( type, obj ) {
- var tmp,
- count = 1,
- defer = jQuery.Deferred(),
- elements = this,
- i = this.length,
- resolve = function() {
- if ( !( --count ) ) {
- defer.resolveWith( elements, [ elements ] );
- }
- };
-
- if ( typeof type !== "string" ) {
- obj = type;
- type = undefined;
- }
- type = type || "fx";
-
- while ( i-- ) {
- tmp = data_priv.get( elements[ i ], type + "queueHooks" );
- if ( tmp && tmp.empty ) {
- count++;
- tmp.empty.add( resolve );
- }
- }
- resolve();
- return defer.promise( obj );
- }
-});
-var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source;
-
-var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
-
-var isHidden = function( elem, el ) {
- // isHidden might be called from jQuery#filter function;
- // in that case, element will be second argument
- elem = el || elem;
- return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
- };
-
-var rcheckableType = (/^(?:checkbox|radio)$/i);
-
-
-
-(function() {
- var fragment = document.createDocumentFragment(),
- div = fragment.appendChild( document.createElement( "div" ) ),
- input = document.createElement( "input" );
-
- // Support: Safari<=5.1
- // Check state lost if the name is set (#11217)
- // Support: Windows Web Apps (WWA)
- // `name` and `type` must use .setAttribute for WWA (#14901)
- input.setAttribute( "type", "radio" );
- input.setAttribute( "checked", "checked" );
- input.setAttribute( "name", "t" );
-
- div.appendChild( input );
-
- // Support: Safari<=5.1, Android<4.2
- // Older WebKit doesn't clone checked state correctly in fragments
- support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
-
- // Support: IE<=11+
- // Make sure textarea (and checkbox) defaultValue is properly cloned
- div.innerHTML = "";
- support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
-})();
-var strundefined = typeof undefined;
-
-
-
-support.focusinBubbles = "onfocusin" in window;
-
-
-var
- rkeyEvent = /^key/,
- rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/,
- rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
- rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
-
-function returnTrue() {
- return true;
-}
-
-function returnFalse() {
- return false;
-}
-
-function safeActiveElement() {
- try {
- return document.activeElement;
- } catch ( err ) { }
-}
-
-/*
- * Helper functions for managing events -- not part of the public interface.
- * Props to Dean Edwards' addEvent library for many of the ideas.
- */
-jQuery.event = {
-
- global: {},
-
- add: function( elem, types, handler, data, selector ) {
-
- var handleObjIn, eventHandle, tmp,
- events, t, handleObj,
- special, handlers, type, namespaces, origType,
- elemData = data_priv.get( elem );
-
- // Don't attach events to noData or text/comment nodes (but allow plain objects)
- if ( !elemData ) {
- return;
- }
-
- // Caller can pass in an object of custom data in lieu of the handler
- if ( handler.handler ) {
- handleObjIn = handler;
- handler = handleObjIn.handler;
- selector = handleObjIn.selector;
- }
-
- // Make sure that the handler has a unique ID, used to find/remove it later
- if ( !handler.guid ) {
- handler.guid = jQuery.guid++;
- }
-
- // Init the element's event structure and main handler, if this is the first
- if ( !(events = elemData.events) ) {
- events = elemData.events = {};
- }
- if ( !(eventHandle = elemData.handle) ) {
- eventHandle = elemData.handle = function( e ) {
- // Discard the second event of a jQuery.event.trigger() and
- // when an event is called after a page has unloaded
- return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
- jQuery.event.dispatch.apply( elem, arguments ) : undefined;
- };
- }
-
- // Handle multiple events separated by a space
- types = ( types || "" ).match( rnotwhite ) || [ "" ];
- t = types.length;
- while ( t-- ) {
- tmp = rtypenamespace.exec( types[t] ) || [];
- type = origType = tmp[1];
- namespaces = ( tmp[2] || "" ).split( "." ).sort();
-
- // There *must* be a type, no attaching namespace-only handlers
- if ( !type ) {
- continue;
- }
-
- // If event changes its type, use the special event handlers for the changed type
- special = jQuery.event.special[ type ] || {};
-
- // If selector defined, determine special event api type, otherwise given type
- type = ( selector ? special.delegateType : special.bindType ) || type;
-
- // Update special based on newly reset type
- special = jQuery.event.special[ type ] || {};
-
- // handleObj is passed to all event handlers
- handleObj = jQuery.extend({
- type: type,
- origType: origType,
- data: data,
- handler: handler,
- guid: handler.guid,
- selector: selector,
- needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
- namespace: namespaces.join(".")
- }, handleObjIn );
-
- // Init the event handler queue if we're the first
- if ( !(handlers = events[ type ]) ) {
- handlers = events[ type ] = [];
- handlers.delegateCount = 0;
-
- // Only use addEventListener if the special events handler returns false
- if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
- if ( elem.addEventListener ) {
- elem.addEventListener( type, eventHandle, false );
- }
- }
- }
-
- if ( special.add ) {
- special.add.call( elem, handleObj );
-
- if ( !handleObj.handler.guid ) {
- handleObj.handler.guid = handler.guid;
- }
- }
-
- // Add to the element's handler list, delegates in front
- if ( selector ) {
- handlers.splice( handlers.delegateCount++, 0, handleObj );
- } else {
- handlers.push( handleObj );
- }
-
- // Keep track of which events have ever been used, for event optimization
- jQuery.event.global[ type ] = true;
- }
-
- },
-
- // Detach an event or set of events from an element
- remove: function( elem, types, handler, selector, mappedTypes ) {
-
- var j, origCount, tmp,
- events, t, handleObj,
- special, handlers, type, namespaces, origType,
- elemData = data_priv.hasData( elem ) && data_priv.get( elem );
-
- if ( !elemData || !(events = elemData.events) ) {
- return;
- }
-
- // Once for each type.namespace in types; type may be omitted
- types = ( types || "" ).match( rnotwhite ) || [ "" ];
- t = types.length;
- while ( t-- ) {
- tmp = rtypenamespace.exec( types[t] ) || [];
- type = origType = tmp[1];
- namespaces = ( tmp[2] || "" ).split( "." ).sort();
-
- // Unbind all events (on this namespace, if provided) for the element
- if ( !type ) {
- for ( type in events ) {
- jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
- }
- continue;
- }
-
- special = jQuery.event.special[ type ] || {};
- type = ( selector ? special.delegateType : special.bindType ) || type;
- handlers = events[ type ] || [];
- tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" );
-
- // Remove matching events
- origCount = j = handlers.length;
- while ( j-- ) {
- handleObj = handlers[ j ];
-
- if ( ( mappedTypes || origType === handleObj.origType ) &&
- ( !handler || handler.guid === handleObj.guid ) &&
- ( !tmp || tmp.test( handleObj.namespace ) ) &&
- ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
- handlers.splice( j, 1 );
-
- if ( handleObj.selector ) {
- handlers.delegateCount--;
- }
- if ( special.remove ) {
- special.remove.call( elem, handleObj );
- }
- }
- }
-
- // Remove generic event handler if we removed something and no more handlers exist
- // (avoids potential for endless recursion during removal of special event handlers)
- if ( origCount && !handlers.length ) {
- if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
- jQuery.removeEvent( elem, type, elemData.handle );
- }
-
- delete events[ type ];
- }
- }
-
- // Remove the expando if it's no longer used
- if ( jQuery.isEmptyObject( events ) ) {
- delete elemData.handle;
- data_priv.remove( elem, "events" );
- }
- },
-
- trigger: function( event, data, elem, onlyHandlers ) {
-
- var i, cur, tmp, bubbleType, ontype, handle, special,
- eventPath = [ elem || document ],
- type = hasOwn.call( event, "type" ) ? event.type : event,
- namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : [];
-
- cur = tmp = elem = elem || document;
-
- // Don't do events on text and comment nodes
- if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
- return;
- }
-
- // focus/blur morphs to focusin/out; ensure we're not firing them right now
- if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
- return;
- }
-
- if ( type.indexOf(".") >= 0 ) {
- // Namespaced trigger; create a regexp to match event type in handle()
- namespaces = type.split(".");
- type = namespaces.shift();
- namespaces.sort();
- }
- ontype = type.indexOf(":") < 0 && "on" + type;
-
- // Caller can pass in a jQuery.Event object, Object, or just an event type string
- event = event[ jQuery.expando ] ?
- event :
- new jQuery.Event( type, typeof event === "object" && event );
-
- // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
- event.isTrigger = onlyHandlers ? 2 : 3;
- event.namespace = namespaces.join(".");
- event.namespace_re = event.namespace ?
- new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
- null;
-
- // Clean up the event in case it is being reused
- event.result = undefined;
- if ( !event.target ) {
- event.target = elem;
- }
-
- // Clone any incoming data and prepend the event, creating the handler arg list
- data = data == null ?
- [ event ] :
- jQuery.makeArray( data, [ event ] );
-
- // Allow special events to draw outside the lines
- special = jQuery.event.special[ type ] || {};
- if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
- return;
- }
-
- // Determine event propagation path in advance, per W3C events spec (#9951)
- // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
- if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
-
- bubbleType = special.delegateType || type;
- if ( !rfocusMorph.test( bubbleType + type ) ) {
- cur = cur.parentNode;
- }
- for ( ; cur; cur = cur.parentNode ) {
- eventPath.push( cur );
- tmp = cur;
- }
-
- // Only add window if we got to document (e.g., not plain obj or detached DOM)
- if ( tmp === (elem.ownerDocument || document) ) {
- eventPath.push( tmp.defaultView || tmp.parentWindow || window );
- }
- }
-
- // Fire handlers on the event path
- i = 0;
- while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {
-
- event.type = i > 1 ?
- bubbleType :
- special.bindType || type;
-
- // jQuery handler
- handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" );
- if ( handle ) {
- handle.apply( cur, data );
- }
-
- // Native handler
- handle = ontype && cur[ ontype ];
- if ( handle && handle.apply && jQuery.acceptData( cur ) ) {
- event.result = handle.apply( cur, data );
- if ( event.result === false ) {
- event.preventDefault();
- }
- }
- }
- event.type = type;
-
- // If nobody prevented the default action, do it now
- if ( !onlyHandlers && !event.isDefaultPrevented() ) {
-
- if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&
- jQuery.acceptData( elem ) ) {
-
- // Call a native DOM method on the target with the same name name as the event.
- // Don't do default actions on window, that's where global variables be (#6170)
- if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {
-
- // Don't re-trigger an onFOO event when we call its FOO() method
- tmp = elem[ ontype ];
-
- if ( tmp ) {
- elem[ ontype ] = null;
- }
-
- // Prevent re-triggering of the same event, since we already bubbled it above
- jQuery.event.triggered = type;
- elem[ type ]();
- jQuery.event.triggered = undefined;
-
- if ( tmp ) {
- elem[ ontype ] = tmp;
- }
- }
- }
- }
-
- return event.result;
- },
-
- dispatch: function( event ) {
-
- // Make a writable jQuery.Event from the native event object
- event = jQuery.event.fix( event );
-
- var i, j, ret, matched, handleObj,
- handlerQueue = [],
- args = slice.call( arguments ),
- handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],
- special = jQuery.event.special[ event.type ] || {};
-
- // Use the fix-ed jQuery.Event rather than the (read-only) native event
- args[0] = event;
- event.delegateTarget = this;
-
- // Call the preDispatch hook for the mapped type, and let it bail if desired
- if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
- return;
- }
-
- // Determine handlers
- handlerQueue = jQuery.event.handlers.call( this, event, handlers );
-
- // Run delegates first; they may want to stop propagation beneath us
- i = 0;
- while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
- event.currentTarget = matched.elem;
-
- j = 0;
- while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
-
- // Triggered event must either 1) have no namespace, or 2) have namespace(s)
- // a subset or equal to those in the bound event (both can have no namespace).
- if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
-
- event.handleObj = handleObj;
- event.data = handleObj.data;
-
- ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
- .apply( matched.elem, args );
-
- if ( ret !== undefined ) {
- if ( (event.result = ret) === false ) {
- event.preventDefault();
- event.stopPropagation();
- }
- }
- }
- }
- }
-
- // Call the postDispatch hook for the mapped type
- if ( special.postDispatch ) {
- special.postDispatch.call( this, event );
- }
-
- return event.result;
- },
-
- handlers: function( event, handlers ) {
- var i, matches, sel, handleObj,
- handlerQueue = [],
- delegateCount = handlers.delegateCount,
- cur = event.target;
-
- // Find delegate handlers
- // Black-hole SVG instance trees (#13180)
- // Avoid non-left-click bubbling in Firefox (#3861)
- if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
-
- for ( ; cur !== this; cur = cur.parentNode || this ) {
-
- // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
- if ( cur.disabled !== true || event.type !== "click" ) {
- matches = [];
- for ( i = 0; i < delegateCount; i++ ) {
- handleObj = handlers[ i ];
-
- // Don't conflict with Object.prototype properties (#13203)
- sel = handleObj.selector + " ";
-
- if ( matches[ sel ] === undefined ) {
- matches[ sel ] = handleObj.needsContext ?
- jQuery( sel, this ).index( cur ) >= 0 :
- jQuery.find( sel, this, null, [ cur ] ).length;
- }
- if ( matches[ sel ] ) {
- matches.push( handleObj );
- }
- }
- if ( matches.length ) {
- handlerQueue.push({ elem: cur, handlers: matches });
- }
- }
- }
- }
-
- // Add the remaining (directly-bound) handlers
- if ( delegateCount < handlers.length ) {
- handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
- }
-
- return handlerQueue;
- },
-
- // Includes some event props shared by KeyEvent and MouseEvent
- props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
-
- fixHooks: {},
-
- keyHooks: {
- props: "char charCode key keyCode".split(" "),
- filter: function( event, original ) {
-
- // Add which for key events
- if ( event.which == null ) {
- event.which = original.charCode != null ? original.charCode : original.keyCode;
- }
-
- return event;
- }
- },
-
- mouseHooks: {
- props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
- filter: function( event, original ) {
- var eventDoc, doc, body,
- button = original.button;
-
- // Calculate pageX/Y if missing and clientX/Y available
- if ( event.pageX == null && original.clientX != null ) {
- eventDoc = event.target.ownerDocument || document;
- doc = eventDoc.documentElement;
- body = eventDoc.body;
-
- event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
- event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 );
- }
-
- // Add which for click: 1 === left; 2 === middle; 3 === right
- // Note: button is not normalized, so don't use it
- if ( !event.which && button !== undefined ) {
- event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
- }
-
- return event;
- }
- },
-
- fix: function( event ) {
- if ( event[ jQuery.expando ] ) {
- return event;
- }
-
- // Create a writable copy of the event object and normalize some properties
- var i, prop, copy,
- type = event.type,
- originalEvent = event,
- fixHook = this.fixHooks[ type ];
-
- if ( !fixHook ) {
- this.fixHooks[ type ] = fixHook =
- rmouseEvent.test( type ) ? this.mouseHooks :
- rkeyEvent.test( type ) ? this.keyHooks :
- {};
- }
- copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
-
- event = new jQuery.Event( originalEvent );
-
- i = copy.length;
- while ( i-- ) {
- prop = copy[ i ];
- event[ prop ] = originalEvent[ prop ];
- }
-
- // Support: Cordova 2.5 (WebKit) (#13255)
- // All events should have a target; Cordova deviceready doesn't
- if ( !event.target ) {
- event.target = document;
- }
-
- // Support: Safari 6.0+, Chrome<28
- // Target should not be a text node (#504, #13143)
- if ( event.target.nodeType === 3 ) {
- event.target = event.target.parentNode;
- }
-
- return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
- },
-
- special: {
- load: {
- // Prevent triggered image.load events from bubbling to window.load
- noBubble: true
- },
- focus: {
- // Fire native event if possible so blur/focus sequence is correct
- trigger: function() {
- if ( this !== safeActiveElement() && this.focus ) {
- this.focus();
- return false;
- }
- },
- delegateType: "focusin"
- },
- blur: {
- trigger: function() {
- if ( this === safeActiveElement() && this.blur ) {
- this.blur();
- return false;
- }
- },
- delegateType: "focusout"
- },
- click: {
- // For checkbox, fire native event so checked state will be right
- trigger: function() {
- if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) {
- this.click();
- return false;
- }
- },
-
- // For cross-browser consistency, don't fire native .click() on links
- _default: function( event ) {
- return jQuery.nodeName( event.target, "a" );
- }
- },
-
- beforeunload: {
- postDispatch: function( event ) {
-
- // Support: Firefox 20+
- // Firefox doesn't alert if the returnValue field is not set.
- if ( event.result !== undefined && event.originalEvent ) {
- event.originalEvent.returnValue = event.result;
- }
- }
- }
- },
-
- simulate: function( type, elem, event, bubble ) {
- // Piggyback on a donor event to simulate a different one.
- // Fake originalEvent to avoid donor's stopPropagation, but if the
- // simulated event prevents default then we do the same on the donor.
- var e = jQuery.extend(
- new jQuery.Event(),
- event,
- {
- type: type,
- isSimulated: true,
- originalEvent: {}
- }
- );
- if ( bubble ) {
- jQuery.event.trigger( e, null, elem );
- } else {
- jQuery.event.dispatch.call( elem, e );
- }
- if ( e.isDefaultPrevented() ) {
- event.preventDefault();
- }
- }
-};
-
-jQuery.removeEvent = function( elem, type, handle ) {
- if ( elem.removeEventListener ) {
- elem.removeEventListener( type, handle, false );
- }
-};
-
-jQuery.Event = function( src, props ) {
- // Allow instantiation without the 'new' keyword
- if ( !(this instanceof jQuery.Event) ) {
- return new jQuery.Event( src, props );
- }
-
- // Event object
- if ( src && src.type ) {
- this.originalEvent = src;
- this.type = src.type;
-
- // Events bubbling up the document may have been marked as prevented
- // by a handler lower down the tree; reflect the correct value.
- this.isDefaultPrevented = src.defaultPrevented ||
- src.defaultPrevented === undefined &&
- // Support: Android<4.0
- src.returnValue === false ?
- returnTrue :
- returnFalse;
-
- // Event type
- } else {
- this.type = src;
- }
-
- // Put explicitly provided properties onto the event object
- if ( props ) {
- jQuery.extend( this, props );
- }
-
- // Create a timestamp if incoming event doesn't have one
- this.timeStamp = src && src.timeStamp || jQuery.now();
-
- // Mark it as fixed
- this[ jQuery.expando ] = true;
-};
-
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
- isDefaultPrevented: returnFalse,
- isPropagationStopped: returnFalse,
- isImmediatePropagationStopped: returnFalse,
-
- preventDefault: function() {
- var e = this.originalEvent;
-
- this.isDefaultPrevented = returnTrue;
-
- if ( e && e.preventDefault ) {
- e.preventDefault();
- }
- },
- stopPropagation: function() {
- var e = this.originalEvent;
-
- this.isPropagationStopped = returnTrue;
-
- if ( e && e.stopPropagation ) {
- e.stopPropagation();
- }
- },
- stopImmediatePropagation: function() {
- var e = this.originalEvent;
-
- this.isImmediatePropagationStopped = returnTrue;
-
- if ( e && e.stopImmediatePropagation ) {
- e.stopImmediatePropagation();
- }
-
- this.stopPropagation();
- }
-};
-
-// Create mouseenter/leave events using mouseover/out and event-time checks
-// Support: Chrome 15+
-jQuery.each({
- mouseenter: "mouseover",
- mouseleave: "mouseout",
- pointerenter: "pointerover",
- pointerleave: "pointerout"
-}, function( orig, fix ) {
- jQuery.event.special[ orig ] = {
- delegateType: fix,
- bindType: fix,
-
- handle: function( event ) {
- var ret,
- target = this,
- related = event.relatedTarget,
- handleObj = event.handleObj;
-
- // For mousenter/leave call the handler if related is outside the target.
- // NB: No relatedTarget if the mouse left/entered the browser window
- if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
- event.type = handleObj.origType;
- ret = handleObj.handler.apply( this, arguments );
- event.type = fix;
- }
- return ret;
- }
- };
-});
-
-// Support: Firefox, Chrome, Safari
-// Create "bubbling" focus and blur events
-if ( !support.focusinBubbles ) {
- jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
-
- // Attach a single capturing handler on the document while someone wants focusin/focusout
- var handler = function( event ) {
- jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
- };
-
- jQuery.event.special[ fix ] = {
- setup: function() {
- var doc = this.ownerDocument || this,
- attaches = data_priv.access( doc, fix );
-
- if ( !attaches ) {
- doc.addEventListener( orig, handler, true );
- }
- data_priv.access( doc, fix, ( attaches || 0 ) + 1 );
- },
- teardown: function() {
- var doc = this.ownerDocument || this,
- attaches = data_priv.access( doc, fix ) - 1;
-
- if ( !attaches ) {
- doc.removeEventListener( orig, handler, true );
- data_priv.remove( doc, fix );
-
- } else {
- data_priv.access( doc, fix, attaches );
- }
- }
- };
- });
-}
-
-jQuery.fn.extend({
-
- on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
- var origFn, type;
-
- // Types can be a map of types/handlers
- if ( typeof types === "object" ) {
- // ( types-Object, selector, data )
- if ( typeof selector !== "string" ) {
- // ( types-Object, data )
- data = data || selector;
- selector = undefined;
- }
- for ( type in types ) {
- this.on( type, selector, data, types[ type ], one );
- }
- return this;
- }
-
- if ( data == null && fn == null ) {
- // ( types, fn )
- fn = selector;
- data = selector = undefined;
- } else if ( fn == null ) {
- if ( typeof selector === "string" ) {
- // ( types, selector, fn )
- fn = data;
- data = undefined;
- } else {
- // ( types, data, fn )
- fn = data;
- data = selector;
- selector = undefined;
- }
- }
- if ( fn === false ) {
- fn = returnFalse;
- } else if ( !fn ) {
- return this;
- }
-
- if ( one === 1 ) {
- origFn = fn;
- fn = function( event ) {
- // Can use an empty set, since event contains the info
- jQuery().off( event );
- return origFn.apply( this, arguments );
- };
- // Use same guid so caller can remove using origFn
- fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
- }
- return this.each( function() {
- jQuery.event.add( this, types, fn, data, selector );
- });
- },
- one: function( types, selector, data, fn ) {
- return this.on( types, selector, data, fn, 1 );
- },
- off: function( types, selector, fn ) {
- var handleObj, type;
- if ( types && types.preventDefault && types.handleObj ) {
- // ( event ) dispatched jQuery.Event
- handleObj = types.handleObj;
- jQuery( types.delegateTarget ).off(
- handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
- handleObj.selector,
- handleObj.handler
- );
- return this;
- }
- if ( typeof types === "object" ) {
- // ( types-object [, selector] )
- for ( type in types ) {
- this.off( type, selector, types[ type ] );
- }
- return this;
- }
- if ( selector === false || typeof selector === "function" ) {
- // ( types [, fn] )
- fn = selector;
- selector = undefined;
- }
- if ( fn === false ) {
- fn = returnFalse;
- }
- return this.each(function() {
- jQuery.event.remove( this, types, fn, selector );
- });
- },
-
- trigger: function( type, data ) {
- return this.each(function() {
- jQuery.event.trigger( type, data, this );
- });
- },
- triggerHandler: function( type, data ) {
- var elem = this[0];
- if ( elem ) {
- return jQuery.event.trigger( type, data, elem, true );
- }
- }
-});
-
-
-var
- rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
- rtagName = /<([\w:]+)/,
- rhtml = /<|?\w+;/,
- rnoInnerhtml = /<(?:script|style|link)/i,
- // checked="checked" or checked
- rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
- rscriptType = /^$|\/(?:java|ecma)script/i,
- rscriptTypeMasked = /^true\/(.*)/,
- rcleanScript = /^\s*\s*$/g,
-
- // We have to close these tags to support XHTML (#13200)
- wrapMap = {
-
- // Support: IE9
- option: [ 1, "", " " ],
-
- thead: [ 1, "" ],
- col: [ 2, "" ],
- tr: [ 2, "" ],
- td: [ 3, "" ],
-
- _default: [ 0, "", "" ]
- };
-
-// Support: IE9
-wrapMap.optgroup = wrapMap.option;
-
-wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
-wrapMap.th = wrapMap.td;
-
-// Support: 1.x compatibility
-// Manipulating tables requires a tbody
-function manipulationTarget( elem, content ) {
- return jQuery.nodeName( elem, "table" ) &&
- jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ?
-
- elem.getElementsByTagName("tbody")[0] ||
- elem.appendChild( elem.ownerDocument.createElement("tbody") ) :
- elem;
-}
-
-// Replace/restore the type attribute of script elements for safe DOM manipulation
-function disableScript( elem ) {
- elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type;
- return elem;
-}
-function restoreScript( elem ) {
- var match = rscriptTypeMasked.exec( elem.type );
-
- if ( match ) {
- elem.type = match[ 1 ];
- } else {
- elem.removeAttribute("type");
- }
-
- return elem;
-}
-
-// Mark scripts as having already been evaluated
-function setGlobalEval( elems, refElements ) {
- var i = 0,
- l = elems.length;
-
- for ( ; i < l; i++ ) {
- data_priv.set(
- elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" )
- );
- }
-}
-
-function cloneCopyEvent( src, dest ) {
- var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
-
- if ( dest.nodeType !== 1 ) {
- return;
- }
-
- // 1. Copy private data: events, handlers, etc.
- if ( data_priv.hasData( src ) ) {
- pdataOld = data_priv.access( src );
- pdataCur = data_priv.set( dest, pdataOld );
- events = pdataOld.events;
-
- if ( events ) {
- delete pdataCur.handle;
- pdataCur.events = {};
-
- for ( type in events ) {
- for ( i = 0, l = events[ type ].length; i < l; i++ ) {
- jQuery.event.add( dest, type, events[ type ][ i ] );
- }
- }
- }
- }
-
- // 2. Copy user data
- if ( data_user.hasData( src ) ) {
- udataOld = data_user.access( src );
- udataCur = jQuery.extend( {}, udataOld );
-
- data_user.set( dest, udataCur );
- }
-}
-
-function getAll( context, tag ) {
- var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) :
- context.querySelectorAll ? context.querySelectorAll( tag || "*" ) :
- [];
-
- return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
- jQuery.merge( [ context ], ret ) :
- ret;
-}
-
-// Fix IE bugs, see support tests
-function fixInput( src, dest ) {
- var nodeName = dest.nodeName.toLowerCase();
-
- // Fails to persist the checked state of a cloned checkbox or radio button.
- if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
- dest.checked = src.checked;
-
- // Fails to return the selected option to the default selected state when cloning options
- } else if ( nodeName === "input" || nodeName === "textarea" ) {
- dest.defaultValue = src.defaultValue;
- }
-}
-
-jQuery.extend({
- clone: function( elem, dataAndEvents, deepDataAndEvents ) {
- var i, l, srcElements, destElements,
- clone = elem.cloneNode( true ),
- inPage = jQuery.contains( elem.ownerDocument, elem );
-
- // Fix IE cloning issues
- if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
- !jQuery.isXMLDoc( elem ) ) {
-
- // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
- destElements = getAll( clone );
- srcElements = getAll( elem );
-
- for ( i = 0, l = srcElements.length; i < l; i++ ) {
- fixInput( srcElements[ i ], destElements[ i ] );
- }
- }
-
- // Copy the events from the original to the clone
- if ( dataAndEvents ) {
- if ( deepDataAndEvents ) {
- srcElements = srcElements || getAll( elem );
- destElements = destElements || getAll( clone );
-
- for ( i = 0, l = srcElements.length; i < l; i++ ) {
- cloneCopyEvent( srcElements[ i ], destElements[ i ] );
- }
- } else {
- cloneCopyEvent( elem, clone );
- }
- }
-
- // Preserve script evaluation history
- destElements = getAll( clone, "script" );
- if ( destElements.length > 0 ) {
- setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
- }
-
- // Return the cloned set
- return clone;
- },
-
- buildFragment: function( elems, context, scripts, selection ) {
- var elem, tmp, tag, wrap, contains, j,
- fragment = context.createDocumentFragment(),
- nodes = [],
- i = 0,
- l = elems.length;
-
- for ( ; i < l; i++ ) {
- elem = elems[ i ];
-
- if ( elem || elem === 0 ) {
-
- // Add nodes directly
- if ( jQuery.type( elem ) === "object" ) {
- // Support: QtWebKit, PhantomJS
- // push.apply(_, arraylike) throws on ancient WebKit
- jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
-
- // Convert non-html into a text node
- } else if ( !rhtml.test( elem ) ) {
- nodes.push( context.createTextNode( elem ) );
-
- // Convert html into DOM nodes
- } else {
- tmp = tmp || fragment.appendChild( context.createElement("div") );
-
- // Deserialize a standard representation
- tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
- wrap = wrapMap[ tag ] || wrapMap._default;
- tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1>$2>" ) + wrap[ 2 ];
-
- // Descend through wrappers to the right content
- j = wrap[ 0 ];
- while ( j-- ) {
- tmp = tmp.lastChild;
- }
-
- // Support: QtWebKit, PhantomJS
- // push.apply(_, arraylike) throws on ancient WebKit
- jQuery.merge( nodes, tmp.childNodes );
-
- // Remember the top-level container
- tmp = fragment.firstChild;
-
- // Ensure the created nodes are orphaned (#12392)
- tmp.textContent = "";
- }
- }
- }
-
- // Remove wrapper from fragment
- fragment.textContent = "";
-
- i = 0;
- while ( (elem = nodes[ i++ ]) ) {
-
- // #4087 - If origin and destination elements are the same, and this is
- // that element, do not do anything
- if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
- continue;
- }
-
- contains = jQuery.contains( elem.ownerDocument, elem );
-
- // Append to fragment
- tmp = getAll( fragment.appendChild( elem ), "script" );
-
- // Preserve script evaluation history
- if ( contains ) {
- setGlobalEval( tmp );
- }
-
- // Capture executables
- if ( scripts ) {
- j = 0;
- while ( (elem = tmp[ j++ ]) ) {
- if ( rscriptType.test( elem.type || "" ) ) {
- scripts.push( elem );
- }
- }
- }
- }
-
- return fragment;
- },
-
- cleanData: function( elems ) {
- var data, elem, type, key,
- special = jQuery.event.special,
- i = 0;
-
- for ( ; (elem = elems[ i ]) !== undefined; i++ ) {
- if ( jQuery.acceptData( elem ) ) {
- key = elem[ data_priv.expando ];
-
- if ( key && (data = data_priv.cache[ key ]) ) {
- if ( data.events ) {
- for ( type in data.events ) {
- if ( special[ type ] ) {
- jQuery.event.remove( elem, type );
-
- // This is a shortcut to avoid jQuery.event.remove's overhead
- } else {
- jQuery.removeEvent( elem, type, data.handle );
- }
- }
- }
- if ( data_priv.cache[ key ] ) {
- // Discard any remaining `private` data
- delete data_priv.cache[ key ];
- }
- }
- }
- // Discard any remaining `user` data
- delete data_user.cache[ elem[ data_user.expando ] ];
- }
- }
-});
-
-jQuery.fn.extend({
- text: function( value ) {
- return access( this, function( value ) {
- return value === undefined ?
- jQuery.text( this ) :
- this.empty().each(function() {
- if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
- this.textContent = value;
- }
- });
- }, null, value, arguments.length );
- },
-
- append: function() {
- return this.domManip( arguments, function( elem ) {
- if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
- var target = manipulationTarget( this, elem );
- target.appendChild( elem );
- }
- });
- },
-
- prepend: function() {
- return this.domManip( arguments, function( elem ) {
- if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
- var target = manipulationTarget( this, elem );
- target.insertBefore( elem, target.firstChild );
- }
- });
- },
-
- before: function() {
- return this.domManip( arguments, function( elem ) {
- if ( this.parentNode ) {
- this.parentNode.insertBefore( elem, this );
- }
- });
- },
-
- after: function() {
- return this.domManip( arguments, function( elem ) {
- if ( this.parentNode ) {
- this.parentNode.insertBefore( elem, this.nextSibling );
- }
- });
- },
-
- remove: function( selector, keepData /* Internal Use Only */ ) {
- var elem,
- elems = selector ? jQuery.filter( selector, this ) : this,
- i = 0;
-
- for ( ; (elem = elems[i]) != null; i++ ) {
- if ( !keepData && elem.nodeType === 1 ) {
- jQuery.cleanData( getAll( elem ) );
- }
-
- if ( elem.parentNode ) {
- if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
- setGlobalEval( getAll( elem, "script" ) );
- }
- elem.parentNode.removeChild( elem );
- }
- }
-
- return this;
- },
-
- empty: function() {
- var elem,
- i = 0;
-
- for ( ; (elem = this[i]) != null; i++ ) {
- if ( elem.nodeType === 1 ) {
-
- // Prevent memory leaks
- jQuery.cleanData( getAll( elem, false ) );
-
- // Remove any remaining nodes
- elem.textContent = "";
- }
- }
-
- return this;
- },
-
- clone: function( dataAndEvents, deepDataAndEvents ) {
- dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
- deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
-
- return this.map(function() {
- return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
- });
- },
-
- html: function( value ) {
- return access( this, function( value ) {
- var elem = this[ 0 ] || {},
- i = 0,
- l = this.length;
-
- if ( value === undefined && elem.nodeType === 1 ) {
- return elem.innerHTML;
- }
-
- // See if we can take a shortcut and just use innerHTML
- if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
- !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
-
- value = value.replace( rxhtmlTag, "<$1>$2>" );
-
- try {
- for ( ; i < l; i++ ) {
- elem = this[ i ] || {};
-
- // Remove element nodes and prevent memory leaks
- if ( elem.nodeType === 1 ) {
- jQuery.cleanData( getAll( elem, false ) );
- elem.innerHTML = value;
- }
- }
-
- elem = 0;
-
- // If using innerHTML throws an exception, use the fallback method
- } catch( e ) {}
- }
-
- if ( elem ) {
- this.empty().append( value );
- }
- }, null, value, arguments.length );
- },
-
- replaceWith: function() {
- var arg = arguments[ 0 ];
-
- // Make the changes, replacing each context element with the new content
- this.domManip( arguments, function( elem ) {
- arg = this.parentNode;
-
- jQuery.cleanData( getAll( this ) );
-
- if ( arg ) {
- arg.replaceChild( elem, this );
- }
- });
-
- // Force removal if there was no new content (e.g., from empty arguments)
- return arg && (arg.length || arg.nodeType) ? this : this.remove();
- },
-
- detach: function( selector ) {
- return this.remove( selector, true );
- },
-
- domManip: function( args, callback ) {
-
- // Flatten any nested arrays
- args = concat.apply( [], args );
-
- var fragment, first, scripts, hasScripts, node, doc,
- i = 0,
- l = this.length,
- set = this,
- iNoClone = l - 1,
- value = args[ 0 ],
- isFunction = jQuery.isFunction( value );
-
- // We can't cloneNode fragments that contain checked, in WebKit
- if ( isFunction ||
- ( l > 1 && typeof value === "string" &&
- !support.checkClone && rchecked.test( value ) ) ) {
- return this.each(function( index ) {
- var self = set.eq( index );
- if ( isFunction ) {
- args[ 0 ] = value.call( this, index, self.html() );
- }
- self.domManip( args, callback );
- });
- }
-
- if ( l ) {
- fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
- first = fragment.firstChild;
-
- if ( fragment.childNodes.length === 1 ) {
- fragment = first;
- }
-
- if ( first ) {
- scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
- hasScripts = scripts.length;
-
- // Use the original fragment for the last item instead of the first because it can end up
- // being emptied incorrectly in certain situations (#8070).
- for ( ; i < l; i++ ) {
- node = fragment;
-
- if ( i !== iNoClone ) {
- node = jQuery.clone( node, true, true );
-
- // Keep references to cloned scripts for later restoration
- if ( hasScripts ) {
- // Support: QtWebKit
- // jQuery.merge because push.apply(_, arraylike) throws
- jQuery.merge( scripts, getAll( node, "script" ) );
- }
- }
-
- callback.call( this[ i ], node, i );
- }
-
- if ( hasScripts ) {
- doc = scripts[ scripts.length - 1 ].ownerDocument;
-
- // Reenable scripts
- jQuery.map( scripts, restoreScript );
-
- // Evaluate executable scripts on first document insertion
- for ( i = 0; i < hasScripts; i++ ) {
- node = scripts[ i ];
- if ( rscriptType.test( node.type || "" ) &&
- !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
-
- if ( node.src ) {
- // Optional AJAX dependency, but won't run scripts if not present
- if ( jQuery._evalUrl ) {
- jQuery._evalUrl( node.src );
- }
- } else {
- jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) );
- }
- }
- }
- }
- }
- }
-
- return this;
- }
-});
-
-jQuery.each({
- appendTo: "append",
- prependTo: "prepend",
- insertBefore: "before",
- insertAfter: "after",
- replaceAll: "replaceWith"
-}, function( name, original ) {
- jQuery.fn[ name ] = function( selector ) {
- var elems,
- ret = [],
- insert = jQuery( selector ),
- last = insert.length - 1,
- i = 0;
-
- for ( ; i <= last; i++ ) {
- elems = i === last ? this : this.clone( true );
- jQuery( insert[ i ] )[ original ]( elems );
-
- // Support: QtWebKit
- // .get() because push.apply(_, arraylike) throws
- push.apply( ret, elems.get() );
- }
-
- return this.pushStack( ret );
- };
-});
-
-
-var iframe,
- elemdisplay = {};
-
-/**
- * Retrieve the actual display of a element
- * @param {String} name nodeName of the element
- * @param {Object} doc Document object
- */
-// Called only from within defaultDisplay
-function actualDisplay( name, doc ) {
- var style,
- elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
-
- // getDefaultComputedStyle might be reliably used only on attached element
- display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ?
-
- // Use of this method is a temporary fix (more like optimization) until something better comes along,
- // since it was removed from specification and supported only in FF
- style.display : jQuery.css( elem[ 0 ], "display" );
-
- // We don't have any data stored on the element,
- // so use "detach" method as fast way to get rid of the element
- elem.detach();
-
- return display;
-}
-
-/**
- * Try to determine the default display value of an element
- * @param {String} nodeName
- */
-function defaultDisplay( nodeName ) {
- var doc = document,
- display = elemdisplay[ nodeName ];
-
- if ( !display ) {
- display = actualDisplay( nodeName, doc );
-
- // If the simple way fails, read from inside an iframe
- if ( display === "none" || !display ) {
-
- // Use the already-created iframe if possible
- iframe = (iframe || jQuery( "" )).appendTo( doc.documentElement );
-
- // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
- doc = iframe[ 0 ].contentDocument;
-
- // Support: IE
- doc.write();
- doc.close();
-
- display = actualDisplay( nodeName, doc );
- iframe.detach();
- }
-
- // Store the correct default display
- elemdisplay[ nodeName ] = display;
- }
-
- return display;
-}
-var rmargin = (/^margin/);
-
-var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
-
-var getStyles = function( elem ) {
- // Support: IE<=11+, Firefox<=30+ (#15098, #14150)
- // IE throws on elements created in popups
- // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
- if ( elem.ownerDocument.defaultView.opener ) {
- return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
- }
-
- return window.getComputedStyle( elem, null );
- };
-
-
-
-function curCSS( elem, name, computed ) {
- var width, minWidth, maxWidth, ret,
- style = elem.style;
-
- computed = computed || getStyles( elem );
-
- // Support: IE9
- // getPropertyValue is only needed for .css('filter') (#12537)
- if ( computed ) {
- ret = computed.getPropertyValue( name ) || computed[ name ];
- }
-
- if ( computed ) {
-
- if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
- ret = jQuery.style( elem, name );
- }
-
- // Support: iOS < 6
- // A tribute to the "awesome hack by Dean Edwards"
- // iOS < 6 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
- // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
- if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
-
- // Remember the original values
- width = style.width;
- minWidth = style.minWidth;
- maxWidth = style.maxWidth;
-
- // Put in the new values to get a computed value out
- style.minWidth = style.maxWidth = style.width = ret;
- ret = computed.width;
-
- // Revert the changed values
- style.width = width;
- style.minWidth = minWidth;
- style.maxWidth = maxWidth;
- }
- }
-
- return ret !== undefined ?
- // Support: IE
- // IE returns zIndex value as an integer.
- ret + "" :
- ret;
-}
-
-
-function addGetHookIf( conditionFn, hookFn ) {
- // Define the hook, we'll check on the first run if it's really needed.
- return {
- get: function() {
- if ( conditionFn() ) {
- // Hook not needed (or it's not possible to use it due
- // to missing dependency), remove it.
- delete this.get;
- return;
- }
-
- // Hook needed; redefine it so that the support test is not executed again.
- return (this.get = hookFn).apply( this, arguments );
- }
- };
-}
-
-
-(function() {
- var pixelPositionVal, boxSizingReliableVal,
- docElem = document.documentElement,
- container = document.createElement( "div" ),
- div = document.createElement( "div" );
-
- if ( !div.style ) {
- return;
- }
-
- // Support: IE9-11+
- // Style of cloned element affects source element cloned (#8908)
- div.style.backgroundClip = "content-box";
- div.cloneNode( true ).style.backgroundClip = "";
- support.clearCloneStyle = div.style.backgroundClip === "content-box";
-
- container.style.cssText = "border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;" +
- "position:absolute";
- container.appendChild( div );
-
- // Executing both pixelPosition & boxSizingReliable tests require only one layout
- // so they're executed at the same time to save the second computation.
- function computePixelPositionAndBoxSizingReliable() {
- div.style.cssText =
- // Support: Firefox<29, Android 2.3
- // Vendor-prefix box-sizing
- "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;" +
- "box-sizing:border-box;display:block;margin-top:1%;top:1%;" +
- "border:1px;padding:1px;width:4px;position:absolute";
- div.innerHTML = "";
- docElem.appendChild( container );
-
- var divStyle = window.getComputedStyle( div, null );
- pixelPositionVal = divStyle.top !== "1%";
- boxSizingReliableVal = divStyle.width === "4px";
-
- docElem.removeChild( container );
- }
-
- // Support: node.js jsdom
- // Don't assume that getComputedStyle is a property of the global object
- if ( window.getComputedStyle ) {
- jQuery.extend( support, {
- pixelPosition: function() {
-
- // This test is executed only once but we still do memoizing
- // since we can use the boxSizingReliable pre-computing.
- // No need to check if the test was already performed, though.
- computePixelPositionAndBoxSizingReliable();
- return pixelPositionVal;
- },
- boxSizingReliable: function() {
- if ( boxSizingReliableVal == null ) {
- computePixelPositionAndBoxSizingReliable();
- }
- return boxSizingReliableVal;
- },
- reliableMarginRight: function() {
-
- // Support: Android 2.3
- // Check if div with explicit width and no margin-right incorrectly
- // gets computed margin-right based on width of container. (#3333)
- // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
- // This support function is only executed once so no memoizing is needed.
- var ret,
- marginDiv = div.appendChild( document.createElement( "div" ) );
-
- // Reset CSS: box-sizing; display; margin; border; padding
- marginDiv.style.cssText = div.style.cssText =
- // Support: Firefox<29, Android 2.3
- // Vendor-prefix box-sizing
- "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" +
- "box-sizing:content-box;display:block;margin:0;border:0;padding:0";
- marginDiv.style.marginRight = marginDiv.style.width = "0";
- div.style.width = "1px";
- docElem.appendChild( container );
-
- ret = !parseFloat( window.getComputedStyle( marginDiv, null ).marginRight );
-
- docElem.removeChild( container );
- div.removeChild( marginDiv );
-
- return ret;
- }
- });
- }
-})();
-
-
-// A method for quickly swapping in/out CSS properties to get correct calculations.
-jQuery.swap = function( elem, options, callback, args ) {
- var ret, name,
- old = {};
-
- // Remember the old values, and insert the new ones
- for ( name in options ) {
- old[ name ] = elem.style[ name ];
- elem.style[ name ] = options[ name ];
- }
-
- ret = callback.apply( elem, args || [] );
-
- // Revert the old values
- for ( name in options ) {
- elem.style[ name ] = old[ name ];
- }
-
- return ret;
-};
-
-
-var
- // Swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
- // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
- rdisplayswap = /^(none|table(?!-c[ea]).+)/,
- rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ),
- rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ),
-
- cssShow = { position: "absolute", visibility: "hidden", display: "block" },
- cssNormalTransform = {
- letterSpacing: "0",
- fontWeight: "400"
- },
-
- cssPrefixes = [ "Webkit", "O", "Moz", "ms" ];
-
-// Return a css property mapped to a potentially vendor prefixed property
-function vendorPropName( style, name ) {
-
- // Shortcut for names that are not vendor prefixed
- if ( name in style ) {
- return name;
- }
-
- // Check for vendor prefixed names
- var capName = name[0].toUpperCase() + name.slice(1),
- origName = name,
- i = cssPrefixes.length;
-
- while ( i-- ) {
- name = cssPrefixes[ i ] + capName;
- if ( name in style ) {
- return name;
- }
- }
-
- return origName;
-}
-
-function setPositiveNumber( elem, value, subtract ) {
- var matches = rnumsplit.exec( value );
- return matches ?
- // Guard against undefined "subtract", e.g., when used as in cssHooks
- Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
- value;
-}
-
-function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
- var i = extra === ( isBorderBox ? "border" : "content" ) ?
- // If we already have the right measurement, avoid augmentation
- 4 :
- // Otherwise initialize for horizontal or vertical properties
- name === "width" ? 1 : 0,
-
- val = 0;
-
- for ( ; i < 4; i += 2 ) {
- // Both box models exclude margin, so add it if we want it
- if ( extra === "margin" ) {
- val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
- }
-
- if ( isBorderBox ) {
- // border-box includes padding, so remove it if we want content
- if ( extra === "content" ) {
- val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
- }
-
- // At this point, extra isn't border nor margin, so remove border
- if ( extra !== "margin" ) {
- val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
- }
- } else {
- // At this point, extra isn't content, so add padding
- val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
-
- // At this point, extra isn't content nor padding, so add border
- if ( extra !== "padding" ) {
- val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
- }
- }
- }
-
- return val;
-}
-
-function getWidthOrHeight( elem, name, extra ) {
-
- // Start with offset property, which is equivalent to the border-box value
- var valueIsBorderBox = true,
- val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
- styles = getStyles( elem ),
- isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
-
- // Some non-html elements return undefined for offsetWidth, so check for null/undefined
- // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
- // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
- if ( val <= 0 || val == null ) {
- // Fall back to computed then uncomputed css if necessary
- val = curCSS( elem, name, styles );
- if ( val < 0 || val == null ) {
- val = elem.style[ name ];
- }
-
- // Computed unit is not pixels. Stop here and return.
- if ( rnumnonpx.test(val) ) {
- return val;
- }
-
- // Check for style in case a browser which returns unreliable values
- // for getComputedStyle silently falls back to the reliable elem.style
- valueIsBorderBox = isBorderBox &&
- ( support.boxSizingReliable() || val === elem.style[ name ] );
-
- // Normalize "", auto, and prepare for extra
- val = parseFloat( val ) || 0;
- }
-
- // Use the active box-sizing model to add/subtract irrelevant styles
- return ( val +
- augmentWidthOrHeight(
- elem,
- name,
- extra || ( isBorderBox ? "border" : "content" ),
- valueIsBorderBox,
- styles
- )
- ) + "px";
-}
-
-function showHide( elements, show ) {
- var display, elem, hidden,
- values = [],
- index = 0,
- length = elements.length;
-
- for ( ; index < length; index++ ) {
- elem = elements[ index ];
- if ( !elem.style ) {
- continue;
- }
-
- values[ index ] = data_priv.get( elem, "olddisplay" );
- display = elem.style.display;
- if ( show ) {
- // Reset the inline display of this element to learn if it is
- // being hidden by cascaded rules or not
- if ( !values[ index ] && display === "none" ) {
- elem.style.display = "";
- }
-
- // Set elements which have been overridden with display: none
- // in a stylesheet to whatever the default browser style is
- // for such an element
- if ( elem.style.display === "" && isHidden( elem ) ) {
- values[ index ] = data_priv.access( elem, "olddisplay", defaultDisplay(elem.nodeName) );
- }
- } else {
- hidden = isHidden( elem );
-
- if ( display !== "none" || !hidden ) {
- data_priv.set( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) );
- }
- }
- }
-
- // Set the display of most of the elements in a second loop
- // to avoid the constant reflow
- for ( index = 0; index < length; index++ ) {
- elem = elements[ index ];
- if ( !elem.style ) {
- continue;
- }
- if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
- elem.style.display = show ? values[ index ] || "" : "none";
- }
- }
-
- return elements;
-}
-
-jQuery.extend({
-
- // Add in style property hooks for overriding the default
- // behavior of getting and setting a style property
- cssHooks: {
- opacity: {
- get: function( elem, computed ) {
- if ( computed ) {
-
- // We should always get a number back from opacity
- var ret = curCSS( elem, "opacity" );
- return ret === "" ? "1" : ret;
- }
- }
- }
- },
-
- // Don't automatically add "px" to these possibly-unitless properties
- cssNumber: {
- "columnCount": true,
- "fillOpacity": true,
- "flexGrow": true,
- "flexShrink": true,
- "fontWeight": true,
- "lineHeight": true,
- "opacity": true,
- "order": true,
- "orphans": true,
- "widows": true,
- "zIndex": true,
- "zoom": true
- },
-
- // Add in properties whose names you wish to fix before
- // setting or getting the value
- cssProps: {
- "float": "cssFloat"
- },
-
- // Get and set the style property on a DOM Node
- style: function( elem, name, value, extra ) {
-
- // Don't set styles on text and comment nodes
- if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
- return;
- }
-
- // Make sure that we're working with the right name
- var ret, type, hooks,
- origName = jQuery.camelCase( name ),
- style = elem.style;
-
- name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
-
- // Gets hook for the prefixed version, then unprefixed version
- hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
-
- // Check if we're setting a value
- if ( value !== undefined ) {
- type = typeof value;
-
- // Convert "+=" or "-=" to relative numbers (#7345)
- if ( type === "string" && (ret = rrelNum.exec( value )) ) {
- value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
- // Fixes bug #9237
- type = "number";
- }
-
- // Make sure that null and NaN values aren't set (#7116)
- if ( value == null || value !== value ) {
- return;
- }
-
- // If a number, add 'px' to the (except for certain CSS properties)
- if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
- value += "px";
- }
-
- // Support: IE9-11+
- // background-* props affect original clone's values
- if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
- style[ name ] = "inherit";
- }
-
- // If a hook was provided, use that value, otherwise just set the specified value
- if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
- style[ name ] = value;
- }
-
- } else {
- // If a hook was provided get the non-computed value from there
- if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
- return ret;
- }
-
- // Otherwise just get the value from the style object
- return style[ name ];
- }
- },
-
- css: function( elem, name, extra, styles ) {
- var val, num, hooks,
- origName = jQuery.camelCase( name );
-
- // Make sure that we're working with the right name
- name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
-
- // Try prefixed name followed by the unprefixed name
- hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
-
- // If a hook was provided get the computed value from there
- if ( hooks && "get" in hooks ) {
- val = hooks.get( elem, true, extra );
- }
-
- // Otherwise, if a way to get the computed value exists, use that
- if ( val === undefined ) {
- val = curCSS( elem, name, styles );
- }
-
- // Convert "normal" to computed value
- if ( val === "normal" && name in cssNormalTransform ) {
- val = cssNormalTransform[ name ];
- }
-
- // Make numeric if forced or a qualifier was provided and val looks numeric
- if ( extra === "" || extra ) {
- num = parseFloat( val );
- return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
- }
- return val;
- }
-});
-
-jQuery.each([ "height", "width" ], function( i, name ) {
- jQuery.cssHooks[ name ] = {
- get: function( elem, computed, extra ) {
- if ( computed ) {
-
- // Certain elements can have dimension info if we invisibly show them
- // but it must have a current display style that would benefit
- return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ?
- jQuery.swap( elem, cssShow, function() {
- return getWidthOrHeight( elem, name, extra );
- }) :
- getWidthOrHeight( elem, name, extra );
- }
- },
-
- set: function( elem, value, extra ) {
- var styles = extra && getStyles( elem );
- return setPositiveNumber( elem, value, extra ?
- augmentWidthOrHeight(
- elem,
- name,
- extra,
- jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
- styles
- ) : 0
- );
- }
- };
-});
-
-// Support: Android 2.3
-jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
- function( elem, computed ) {
- if ( computed ) {
- return jQuery.swap( elem, { "display": "inline-block" },
- curCSS, [ elem, "marginRight" ] );
- }
- }
-);
-
-// These hooks are used by animate to expand properties
-jQuery.each({
- margin: "",
- padding: "",
- border: "Width"
-}, function( prefix, suffix ) {
- jQuery.cssHooks[ prefix + suffix ] = {
- expand: function( value ) {
- var i = 0,
- expanded = {},
-
- // Assumes a single number if not a string
- parts = typeof value === "string" ? value.split(" ") : [ value ];
-
- for ( ; i < 4; i++ ) {
- expanded[ prefix + cssExpand[ i ] + suffix ] =
- parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
- }
-
- return expanded;
- }
- };
-
- if ( !rmargin.test( prefix ) ) {
- jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
- }
-});
-
-jQuery.fn.extend({
- css: function( name, value ) {
- return access( this, function( elem, name, value ) {
- var styles, len,
- map = {},
- i = 0;
-
- if ( jQuery.isArray( name ) ) {
- styles = getStyles( elem );
- len = name.length;
-
- for ( ; i < len; i++ ) {
- map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
- }
-
- return map;
- }
-
- return value !== undefined ?
- jQuery.style( elem, name, value ) :
- jQuery.css( elem, name );
- }, name, value, arguments.length > 1 );
- },
- show: function() {
- return showHide( this, true );
- },
- hide: function() {
- return showHide( this );
- },
- toggle: function( state ) {
- if ( typeof state === "boolean" ) {
- return state ? this.show() : this.hide();
- }
-
- return this.each(function() {
- if ( isHidden( this ) ) {
- jQuery( this ).show();
- } else {
- jQuery( this ).hide();
- }
- });
- }
-});
-
-
-function Tween( elem, options, prop, end, easing ) {
- return new Tween.prototype.init( elem, options, prop, end, easing );
-}
-jQuery.Tween = Tween;
-
-Tween.prototype = {
- constructor: Tween,
- init: function( elem, options, prop, end, easing, unit ) {
- this.elem = elem;
- this.prop = prop;
- this.easing = easing || "swing";
- this.options = options;
- this.start = this.now = this.cur();
- this.end = end;
- this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
- },
- cur: function() {
- var hooks = Tween.propHooks[ this.prop ];
-
- return hooks && hooks.get ?
- hooks.get( this ) :
- Tween.propHooks._default.get( this );
- },
- run: function( percent ) {
- var eased,
- hooks = Tween.propHooks[ this.prop ];
-
- if ( this.options.duration ) {
- this.pos = eased = jQuery.easing[ this.easing ](
- percent, this.options.duration * percent, 0, 1, this.options.duration
- );
- } else {
- this.pos = eased = percent;
- }
- this.now = ( this.end - this.start ) * eased + this.start;
-
- if ( this.options.step ) {
- this.options.step.call( this.elem, this.now, this );
- }
-
- if ( hooks && hooks.set ) {
- hooks.set( this );
- } else {
- Tween.propHooks._default.set( this );
- }
- return this;
- }
-};
-
-Tween.prototype.init.prototype = Tween.prototype;
-
-Tween.propHooks = {
- _default: {
- get: function( tween ) {
- var result;
-
- if ( tween.elem[ tween.prop ] != null &&
- (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
- return tween.elem[ tween.prop ];
- }
-
- // Passing an empty string as a 3rd parameter to .css will automatically
- // attempt a parseFloat and fallback to a string if the parse fails.
- // Simple values such as "10px" are parsed to Float;
- // complex values such as "rotate(1rad)" are returned as-is.
- result = jQuery.css( tween.elem, tween.prop, "" );
- // Empty strings, null, undefined and "auto" are converted to 0.
- return !result || result === "auto" ? 0 : result;
- },
- set: function( tween ) {
- // Use step hook for back compat.
- // Use cssHook if its there.
- // Use .style if available and use plain properties where available.
- if ( jQuery.fx.step[ tween.prop ] ) {
- jQuery.fx.step[ tween.prop ]( tween );
- } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
- jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
- } else {
- tween.elem[ tween.prop ] = tween.now;
- }
- }
- }
-};
-
-// Support: IE9
-// Panic based approach to setting things on disconnected nodes
-Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
- set: function( tween ) {
- if ( tween.elem.nodeType && tween.elem.parentNode ) {
- tween.elem[ tween.prop ] = tween.now;
- }
- }
-};
-
-jQuery.easing = {
- linear: function( p ) {
- return p;
- },
- swing: function( p ) {
- return 0.5 - Math.cos( p * Math.PI ) / 2;
- }
-};
-
-jQuery.fx = Tween.prototype.init;
-
-// Back Compat <1.8 extension point
-jQuery.fx.step = {};
-
-
-
-
-var
- fxNow, timerId,
- rfxtypes = /^(?:toggle|show|hide)$/,
- rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ),
- rrun = /queueHooks$/,
- animationPrefilters = [ defaultPrefilter ],
- tweeners = {
- "*": [ function( prop, value ) {
- var tween = this.createTween( prop, value ),
- target = tween.cur(),
- parts = rfxnum.exec( value ),
- unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
-
- // Starting value computation is required for potential unit mismatches
- start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) &&
- rfxnum.exec( jQuery.css( tween.elem, prop ) ),
- scale = 1,
- maxIterations = 20;
-
- if ( start && start[ 3 ] !== unit ) {
- // Trust units reported by jQuery.css
- unit = unit || start[ 3 ];
-
- // Make sure we update the tween properties later on
- parts = parts || [];
-
- // Iteratively approximate from a nonzero starting point
- start = +target || 1;
-
- do {
- // If previous iteration zeroed out, double until we get *something*.
- // Use string for doubling so we don't accidentally see scale as unchanged below
- scale = scale || ".5";
-
- // Adjust and apply
- start = start / scale;
- jQuery.style( tween.elem, prop, start + unit );
-
- // Update scale, tolerating zero or NaN from tween.cur(),
- // break the loop if scale is unchanged or perfect, or if we've just had enough
- } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
- }
-
- // Update tween properties
- if ( parts ) {
- start = tween.start = +start || +target || 0;
- tween.unit = unit;
- // If a +=/-= token was provided, we're doing a relative animation
- tween.end = parts[ 1 ] ?
- start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :
- +parts[ 2 ];
- }
-
- return tween;
- } ]
- };
-
-// Animations created synchronously will run synchronously
-function createFxNow() {
- setTimeout(function() {
- fxNow = undefined;
- });
- return ( fxNow = jQuery.now() );
-}
-
-// Generate parameters to create a standard animation
-function genFx( type, includeWidth ) {
- var which,
- i = 0,
- attrs = { height: type };
-
- // If we include width, step value is 1 to do all cssExpand values,
- // otherwise step value is 2 to skip over Left and Right
- includeWidth = includeWidth ? 1 : 0;
- for ( ; i < 4 ; i += 2 - includeWidth ) {
- which = cssExpand[ i ];
- attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
- }
-
- if ( includeWidth ) {
- attrs.opacity = attrs.width = type;
- }
-
- return attrs;
-}
-
-function createTween( value, prop, animation ) {
- var tween,
- collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
- index = 0,
- length = collection.length;
- for ( ; index < length; index++ ) {
- if ( (tween = collection[ index ].call( animation, prop, value )) ) {
-
- // We're done with this property
- return tween;
- }
- }
-}
-
-function defaultPrefilter( elem, props, opts ) {
- /* jshint validthis: true */
- var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,
- anim = this,
- orig = {},
- style = elem.style,
- hidden = elem.nodeType && isHidden( elem ),
- dataShow = data_priv.get( elem, "fxshow" );
-
- // Handle queue: false promises
- if ( !opts.queue ) {
- hooks = jQuery._queueHooks( elem, "fx" );
- if ( hooks.unqueued == null ) {
- hooks.unqueued = 0;
- oldfire = hooks.empty.fire;
- hooks.empty.fire = function() {
- if ( !hooks.unqueued ) {
- oldfire();
- }
- };
- }
- hooks.unqueued++;
-
- anim.always(function() {
- // Ensure the complete handler is called before this completes
- anim.always(function() {
- hooks.unqueued--;
- if ( !jQuery.queue( elem, "fx" ).length ) {
- hooks.empty.fire();
- }
- });
- });
- }
-
- // Height/width overflow pass
- if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
- // Make sure that nothing sneaks out
- // Record all 3 overflow attributes because IE9-10 do not
- // change the overflow attribute when overflowX and
- // overflowY are set to the same value
- opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
-
- // Set display property to inline-block for height/width
- // animations on inline elements that are having width/height animated
- display = jQuery.css( elem, "display" );
-
- // Test default display if display is currently "none"
- checkDisplay = display === "none" ?
- data_priv.get( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;
-
- if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {
- style.display = "inline-block";
- }
- }
-
- if ( opts.overflow ) {
- style.overflow = "hidden";
- anim.always(function() {
- style.overflow = opts.overflow[ 0 ];
- style.overflowX = opts.overflow[ 1 ];
- style.overflowY = opts.overflow[ 2 ];
- });
- }
-
- // show/hide pass
- for ( prop in props ) {
- value = props[ prop ];
- if ( rfxtypes.exec( value ) ) {
- delete props[ prop ];
- toggle = toggle || value === "toggle";
- if ( value === ( hidden ? "hide" : "show" ) ) {
-
- // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden
- if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
- hidden = true;
- } else {
- continue;
- }
- }
- orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
-
- // Any non-fx value stops us from restoring the original display value
- } else {
- display = undefined;
- }
- }
-
- if ( !jQuery.isEmptyObject( orig ) ) {
- if ( dataShow ) {
- if ( "hidden" in dataShow ) {
- hidden = dataShow.hidden;
- }
- } else {
- dataShow = data_priv.access( elem, "fxshow", {} );
- }
-
- // Store state if its toggle - enables .stop().toggle() to "reverse"
- if ( toggle ) {
- dataShow.hidden = !hidden;
- }
- if ( hidden ) {
- jQuery( elem ).show();
- } else {
- anim.done(function() {
- jQuery( elem ).hide();
- });
- }
- anim.done(function() {
- var prop;
-
- data_priv.remove( elem, "fxshow" );
- for ( prop in orig ) {
- jQuery.style( elem, prop, orig[ prop ] );
- }
- });
- for ( prop in orig ) {
- tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
-
- if ( !( prop in dataShow ) ) {
- dataShow[ prop ] = tween.start;
- if ( hidden ) {
- tween.end = tween.start;
- tween.start = prop === "width" || prop === "height" ? 1 : 0;
- }
- }
- }
-
- // If this is a noop like .hide().hide(), restore an overwritten display value
- } else if ( (display === "none" ? defaultDisplay( elem.nodeName ) : display) === "inline" ) {
- style.display = display;
- }
-}
-
-function propFilter( props, specialEasing ) {
- var index, name, easing, value, hooks;
-
- // camelCase, specialEasing and expand cssHook pass
- for ( index in props ) {
- name = jQuery.camelCase( index );
- easing = specialEasing[ name ];
- value = props[ index ];
- if ( jQuery.isArray( value ) ) {
- easing = value[ 1 ];
- value = props[ index ] = value[ 0 ];
- }
-
- if ( index !== name ) {
- props[ name ] = value;
- delete props[ index ];
- }
-
- hooks = jQuery.cssHooks[ name ];
- if ( hooks && "expand" in hooks ) {
- value = hooks.expand( value );
- delete props[ name ];
-
- // Not quite $.extend, this won't overwrite existing keys.
- // Reusing 'index' because we have the correct "name"
- for ( index in value ) {
- if ( !( index in props ) ) {
- props[ index ] = value[ index ];
- specialEasing[ index ] = easing;
- }
- }
- } else {
- specialEasing[ name ] = easing;
- }
- }
-}
-
-function Animation( elem, properties, options ) {
- var result,
- stopped,
- index = 0,
- length = animationPrefilters.length,
- deferred = jQuery.Deferred().always( function() {
- // Don't match elem in the :animated selector
- delete tick.elem;
- }),
- tick = function() {
- if ( stopped ) {
- return false;
- }
- var currentTime = fxNow || createFxNow(),
- remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
- // Support: Android 2.3
- // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
- temp = remaining / animation.duration || 0,
- percent = 1 - temp,
- index = 0,
- length = animation.tweens.length;
-
- for ( ; index < length ; index++ ) {
- animation.tweens[ index ].run( percent );
- }
-
- deferred.notifyWith( elem, [ animation, percent, remaining ]);
-
- if ( percent < 1 && length ) {
- return remaining;
- } else {
- deferred.resolveWith( elem, [ animation ] );
- return false;
- }
- },
- animation = deferred.promise({
- elem: elem,
- props: jQuery.extend( {}, properties ),
- opts: jQuery.extend( true, { specialEasing: {} }, options ),
- originalProperties: properties,
- originalOptions: options,
- startTime: fxNow || createFxNow(),
- duration: options.duration,
- tweens: [],
- createTween: function( prop, end ) {
- var tween = jQuery.Tween( elem, animation.opts, prop, end,
- animation.opts.specialEasing[ prop ] || animation.opts.easing );
- animation.tweens.push( tween );
- return tween;
- },
- stop: function( gotoEnd ) {
- var index = 0,
- // If we are going to the end, we want to run all the tweens
- // otherwise we skip this part
- length = gotoEnd ? animation.tweens.length : 0;
- if ( stopped ) {
- return this;
- }
- stopped = true;
- for ( ; index < length ; index++ ) {
- animation.tweens[ index ].run( 1 );
- }
-
- // Resolve when we played the last frame; otherwise, reject
- if ( gotoEnd ) {
- deferred.resolveWith( elem, [ animation, gotoEnd ] );
- } else {
- deferred.rejectWith( elem, [ animation, gotoEnd ] );
- }
- return this;
- }
- }),
- props = animation.props;
-
- propFilter( props, animation.opts.specialEasing );
-
- for ( ; index < length ; index++ ) {
- result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
- if ( result ) {
- return result;
- }
- }
-
- jQuery.map( props, createTween, animation );
-
- if ( jQuery.isFunction( animation.opts.start ) ) {
- animation.opts.start.call( elem, animation );
- }
-
- jQuery.fx.timer(
- jQuery.extend( tick, {
- elem: elem,
- anim: animation,
- queue: animation.opts.queue
- })
- );
-
- // attach callbacks from options
- return animation.progress( animation.opts.progress )
- .done( animation.opts.done, animation.opts.complete )
- .fail( animation.opts.fail )
- .always( animation.opts.always );
-}
-
-jQuery.Animation = jQuery.extend( Animation, {
-
- tweener: function( props, callback ) {
- if ( jQuery.isFunction( props ) ) {
- callback = props;
- props = [ "*" ];
- } else {
- props = props.split(" ");
- }
-
- var prop,
- index = 0,
- length = props.length;
-
- for ( ; index < length ; index++ ) {
- prop = props[ index ];
- tweeners[ prop ] = tweeners[ prop ] || [];
- tweeners[ prop ].unshift( callback );
- }
- },
-
- prefilter: function( callback, prepend ) {
- if ( prepend ) {
- animationPrefilters.unshift( callback );
- } else {
- animationPrefilters.push( callback );
- }
- }
-});
-
-jQuery.speed = function( speed, easing, fn ) {
- var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
- complete: fn || !fn && easing ||
- jQuery.isFunction( speed ) && speed,
- duration: speed,
- easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
- };
-
- opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
- opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
-
- // Normalize opt.queue - true/undefined/null -> "fx"
- if ( opt.queue == null || opt.queue === true ) {
- opt.queue = "fx";
- }
-
- // Queueing
- opt.old = opt.complete;
-
- opt.complete = function() {
- if ( jQuery.isFunction( opt.old ) ) {
- opt.old.call( this );
- }
-
- if ( opt.queue ) {
- jQuery.dequeue( this, opt.queue );
- }
- };
-
- return opt;
-};
-
-jQuery.fn.extend({
- fadeTo: function( speed, to, easing, callback ) {
-
- // Show any hidden elements after setting opacity to 0
- return this.filter( isHidden ).css( "opacity", 0 ).show()
-
- // Animate to the value specified
- .end().animate({ opacity: to }, speed, easing, callback );
- },
- animate: function( prop, speed, easing, callback ) {
- var empty = jQuery.isEmptyObject( prop ),
- optall = jQuery.speed( speed, easing, callback ),
- doAnimation = function() {
- // Operate on a copy of prop so per-property easing won't be lost
- var anim = Animation( this, jQuery.extend( {}, prop ), optall );
-
- // Empty animations, or finishing resolves immediately
- if ( empty || data_priv.get( this, "finish" ) ) {
- anim.stop( true );
- }
- };
- doAnimation.finish = doAnimation;
-
- return empty || optall.queue === false ?
- this.each( doAnimation ) :
- this.queue( optall.queue, doAnimation );
- },
- stop: function( type, clearQueue, gotoEnd ) {
- var stopQueue = function( hooks ) {
- var stop = hooks.stop;
- delete hooks.stop;
- stop( gotoEnd );
- };
-
- if ( typeof type !== "string" ) {
- gotoEnd = clearQueue;
- clearQueue = type;
- type = undefined;
- }
- if ( clearQueue && type !== false ) {
- this.queue( type || "fx", [] );
- }
-
- return this.each(function() {
- var dequeue = true,
- index = type != null && type + "queueHooks",
- timers = jQuery.timers,
- data = data_priv.get( this );
-
- if ( index ) {
- if ( data[ index ] && data[ index ].stop ) {
- stopQueue( data[ index ] );
- }
- } else {
- for ( index in data ) {
- if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
- stopQueue( data[ index ] );
- }
- }
- }
-
- for ( index = timers.length; index--; ) {
- if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
- timers[ index ].anim.stop( gotoEnd );
- dequeue = false;
- timers.splice( index, 1 );
- }
- }
-
- // Start the next in the queue if the last step wasn't forced.
- // Timers currently will call their complete callbacks, which
- // will dequeue but only if they were gotoEnd.
- if ( dequeue || !gotoEnd ) {
- jQuery.dequeue( this, type );
- }
- });
- },
- finish: function( type ) {
- if ( type !== false ) {
- type = type || "fx";
- }
- return this.each(function() {
- var index,
- data = data_priv.get( this ),
- queue = data[ type + "queue" ],
- hooks = data[ type + "queueHooks" ],
- timers = jQuery.timers,
- length = queue ? queue.length : 0;
-
- // Enable finishing flag on private data
- data.finish = true;
-
- // Empty the queue first
- jQuery.queue( this, type, [] );
-
- if ( hooks && hooks.stop ) {
- hooks.stop.call( this, true );
- }
-
- // Look for any active animations, and finish them
- for ( index = timers.length; index--; ) {
- if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
- timers[ index ].anim.stop( true );
- timers.splice( index, 1 );
- }
- }
-
- // Look for any animations in the old queue and finish them
- for ( index = 0; index < length; index++ ) {
- if ( queue[ index ] && queue[ index ].finish ) {
- queue[ index ].finish.call( this );
- }
- }
-
- // Turn off finishing flag
- delete data.finish;
- });
- }
-});
-
-jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
- var cssFn = jQuery.fn[ name ];
- jQuery.fn[ name ] = function( speed, easing, callback ) {
- return speed == null || typeof speed === "boolean" ?
- cssFn.apply( this, arguments ) :
- this.animate( genFx( name, true ), speed, easing, callback );
- };
-});
-
-// Generate shortcuts for custom animations
-jQuery.each({
- slideDown: genFx("show"),
- slideUp: genFx("hide"),
- slideToggle: genFx("toggle"),
- fadeIn: { opacity: "show" },
- fadeOut: { opacity: "hide" },
- fadeToggle: { opacity: "toggle" }
-}, function( name, props ) {
- jQuery.fn[ name ] = function( speed, easing, callback ) {
- return this.animate( props, speed, easing, callback );
- };
-});
-
-jQuery.timers = [];
-jQuery.fx.tick = function() {
- var timer,
- i = 0,
- timers = jQuery.timers;
-
- fxNow = jQuery.now();
-
- for ( ; i < timers.length; i++ ) {
- timer = timers[ i ];
- // Checks the timer has not already been removed
- if ( !timer() && timers[ i ] === timer ) {
- timers.splice( i--, 1 );
- }
- }
-
- if ( !timers.length ) {
- jQuery.fx.stop();
- }
- fxNow = undefined;
-};
-
-jQuery.fx.timer = function( timer ) {
- jQuery.timers.push( timer );
- if ( timer() ) {
- jQuery.fx.start();
- } else {
- jQuery.timers.pop();
- }
-};
-
-jQuery.fx.interval = 13;
-
-jQuery.fx.start = function() {
- if ( !timerId ) {
- timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
- }
-};
-
-jQuery.fx.stop = function() {
- clearInterval( timerId );
- timerId = null;
-};
-
-jQuery.fx.speeds = {
- slow: 600,
- fast: 200,
- // Default speed
- _default: 400
-};
-
-
-// Based off of the plugin by Clint Helfers, with permission.
-// http://blindsignals.com/index.php/2009/07/jquery-delay/
-jQuery.fn.delay = function( time, type ) {
- time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
- type = type || "fx";
-
- return this.queue( type, function( next, hooks ) {
- var timeout = setTimeout( next, time );
- hooks.stop = function() {
- clearTimeout( timeout );
- };
- });
-};
-
-
-(function() {
- var input = document.createElement( "input" ),
- select = document.createElement( "select" ),
- opt = select.appendChild( document.createElement( "option" ) );
-
- input.type = "checkbox";
-
- // Support: iOS<=5.1, Android<=4.2+
- // Default value for a checkbox should be "on"
- support.checkOn = input.value !== "";
-
- // Support: IE<=11+
- // Must access selectedIndex to make default options select
- support.optSelected = opt.selected;
-
- // Support: Android<=2.3
- // Options inside disabled selects are incorrectly marked as disabled
- select.disabled = true;
- support.optDisabled = !opt.disabled;
-
- // Support: IE<=11+
- // An input loses its value after becoming a radio
- input = document.createElement( "input" );
- input.value = "t";
- input.type = "radio";
- support.radioValue = input.value === "t";
-})();
-
-
-var nodeHook, boolHook,
- attrHandle = jQuery.expr.attrHandle;
-
-jQuery.fn.extend({
- attr: function( name, value ) {
- return access( this, jQuery.attr, name, value, arguments.length > 1 );
- },
-
- removeAttr: function( name ) {
- return this.each(function() {
- jQuery.removeAttr( this, name );
- });
- }
-});
-
-jQuery.extend({
- attr: function( elem, name, value ) {
- var hooks, ret,
- nType = elem.nodeType;
-
- // don't get/set attributes on text, comment and attribute nodes
- if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
- return;
- }
-
- // Fallback to prop when attributes are not supported
- if ( typeof elem.getAttribute === strundefined ) {
- return jQuery.prop( elem, name, value );
- }
-
- // All attributes are lowercase
- // Grab necessary hook if one is defined
- if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
- name = name.toLowerCase();
- hooks = jQuery.attrHooks[ name ] ||
- ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
- }
-
- if ( value !== undefined ) {
-
- if ( value === null ) {
- jQuery.removeAttr( elem, name );
-
- } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
- return ret;
-
- } else {
- elem.setAttribute( name, value + "" );
- return value;
- }
-
- } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
- return ret;
-
- } else {
- ret = jQuery.find.attr( elem, name );
-
- // Non-existent attributes return null, we normalize to undefined
- return ret == null ?
- undefined :
- ret;
- }
- },
-
- removeAttr: function( elem, value ) {
- var name, propName,
- i = 0,
- attrNames = value && value.match( rnotwhite );
-
- if ( attrNames && elem.nodeType === 1 ) {
- while ( (name = attrNames[i++]) ) {
- propName = jQuery.propFix[ name ] || name;
-
- // Boolean attributes get special treatment (#10870)
- if ( jQuery.expr.match.bool.test( name ) ) {
- // Set corresponding property to false
- elem[ propName ] = false;
- }
-
- elem.removeAttribute( name );
- }
- }
- },
-
- attrHooks: {
- type: {
- set: function( elem, value ) {
- if ( !support.radioValue && value === "radio" &&
- jQuery.nodeName( elem, "input" ) ) {
- var val = elem.value;
- elem.setAttribute( "type", value );
- if ( val ) {
- elem.value = val;
- }
- return value;
- }
- }
- }
- }
-});
-
-// Hooks for boolean attributes
-boolHook = {
- set: function( elem, value, name ) {
- if ( value === false ) {
- // Remove boolean attributes when set to false
- jQuery.removeAttr( elem, name );
- } else {
- elem.setAttribute( name, name );
- }
- return name;
- }
-};
-jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
- var getter = attrHandle[ name ] || jQuery.find.attr;
-
- attrHandle[ name ] = function( elem, name, isXML ) {
- var ret, handle;
- if ( !isXML ) {
- // Avoid an infinite loop by temporarily removing this function from the getter
- handle = attrHandle[ name ];
- attrHandle[ name ] = ret;
- ret = getter( elem, name, isXML ) != null ?
- name.toLowerCase() :
- null;
- attrHandle[ name ] = handle;
- }
- return ret;
- };
-});
-
-
-
-
-var rfocusable = /^(?:input|select|textarea|button)$/i;
-
-jQuery.fn.extend({
- prop: function( name, value ) {
- return access( this, jQuery.prop, name, value, arguments.length > 1 );
- },
-
- removeProp: function( name ) {
- return this.each(function() {
- delete this[ jQuery.propFix[ name ] || name ];
- });
- }
-});
-
-jQuery.extend({
- propFix: {
- "for": "htmlFor",
- "class": "className"
- },
-
- prop: function( elem, name, value ) {
- var ret, hooks, notxml,
- nType = elem.nodeType;
-
- // Don't get/set properties on text, comment and attribute nodes
- if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
- return;
- }
-
- notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
-
- if ( notxml ) {
- // Fix name and attach hooks
- name = jQuery.propFix[ name ] || name;
- hooks = jQuery.propHooks[ name ];
- }
-
- if ( value !== undefined ) {
- return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?
- ret :
- ( elem[ name ] = value );
-
- } else {
- return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ?
- ret :
- elem[ name ];
- }
- },
-
- propHooks: {
- tabIndex: {
- get: function( elem ) {
- return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName ) || elem.href ?
- elem.tabIndex :
- -1;
- }
- }
- }
-});
-
-if ( !support.optSelected ) {
- jQuery.propHooks.selected = {
- get: function( elem ) {
- var parent = elem.parentNode;
- if ( parent && parent.parentNode ) {
- parent.parentNode.selectedIndex;
- }
- return null;
- }
- };
-}
-
-jQuery.each([
- "tabIndex",
- "readOnly",
- "maxLength",
- "cellSpacing",
- "cellPadding",
- "rowSpan",
- "colSpan",
- "useMap",
- "frameBorder",
- "contentEditable"
-], function() {
- jQuery.propFix[ this.toLowerCase() ] = this;
-});
-
-
-
-
-var rclass = /[\t\r\n\f]/g;
-
-jQuery.fn.extend({
- addClass: function( value ) {
- var classes, elem, cur, clazz, j, finalValue,
- proceed = typeof value === "string" && value,
- i = 0,
- len = this.length;
-
- if ( jQuery.isFunction( value ) ) {
- return this.each(function( j ) {
- jQuery( this ).addClass( value.call( this, j, this.className ) );
- });
- }
-
- if ( proceed ) {
- // The disjunction here is for better compressibility (see removeClass)
- classes = ( value || "" ).match( rnotwhite ) || [];
-
- for ( ; i < len; i++ ) {
- elem = this[ i ];
- cur = elem.nodeType === 1 && ( elem.className ?
- ( " " + elem.className + " " ).replace( rclass, " " ) :
- " "
- );
-
- if ( cur ) {
- j = 0;
- while ( (clazz = classes[j++]) ) {
- if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
- cur += clazz + " ";
- }
- }
-
- // only assign if different to avoid unneeded rendering.
- finalValue = jQuery.trim( cur );
- if ( elem.className !== finalValue ) {
- elem.className = finalValue;
- }
- }
- }
- }
-
- return this;
- },
-
- removeClass: function( value ) {
- var classes, elem, cur, clazz, j, finalValue,
- proceed = arguments.length === 0 || typeof value === "string" && value,
- i = 0,
- len = this.length;
-
- if ( jQuery.isFunction( value ) ) {
- return this.each(function( j ) {
- jQuery( this ).removeClass( value.call( this, j, this.className ) );
- });
- }
- if ( proceed ) {
- classes = ( value || "" ).match( rnotwhite ) || [];
-
- for ( ; i < len; i++ ) {
- elem = this[ i ];
- // This expression is here for better compressibility (see addClass)
- cur = elem.nodeType === 1 && ( elem.className ?
- ( " " + elem.className + " " ).replace( rclass, " " ) :
- ""
- );
-
- if ( cur ) {
- j = 0;
- while ( (clazz = classes[j++]) ) {
- // Remove *all* instances
- while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {
- cur = cur.replace( " " + clazz + " ", " " );
- }
- }
-
- // Only assign if different to avoid unneeded rendering.
- finalValue = value ? jQuery.trim( cur ) : "";
- if ( elem.className !== finalValue ) {
- elem.className = finalValue;
- }
- }
- }
- }
-
- return this;
- },
-
- toggleClass: function( value, stateVal ) {
- var type = typeof value;
-
- if ( typeof stateVal === "boolean" && type === "string" ) {
- return stateVal ? this.addClass( value ) : this.removeClass( value );
- }
-
- if ( jQuery.isFunction( value ) ) {
- return this.each(function( i ) {
- jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
- });
- }
-
- return this.each(function() {
- if ( type === "string" ) {
- // Toggle individual class names
- var className,
- i = 0,
- self = jQuery( this ),
- classNames = value.match( rnotwhite ) || [];
-
- while ( (className = classNames[ i++ ]) ) {
- // Check each className given, space separated list
- if ( self.hasClass( className ) ) {
- self.removeClass( className );
- } else {
- self.addClass( className );
- }
- }
-
- // Toggle whole class name
- } else if ( type === strundefined || type === "boolean" ) {
- if ( this.className ) {
- // store className if set
- data_priv.set( this, "__className__", this.className );
- }
-
- // If the element has a class name or if we're passed `false`,
- // then remove the whole classname (if there was one, the above saved it).
- // Otherwise bring back whatever was previously saved (if anything),
- // falling back to the empty string if nothing was stored.
- this.className = this.className || value === false ? "" : data_priv.get( this, "__className__" ) || "";
- }
- });
- },
-
- hasClass: function( selector ) {
- var className = " " + selector + " ",
- i = 0,
- l = this.length;
- for ( ; i < l; i++ ) {
- if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
- return true;
- }
- }
-
- return false;
- }
-});
-
-
-
-
-var rreturn = /\r/g;
-
-jQuery.fn.extend({
- val: function( value ) {
- var hooks, ret, isFunction,
- elem = this[0];
-
- if ( !arguments.length ) {
- if ( elem ) {
- hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
-
- if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
- return ret;
- }
-
- ret = elem.value;
-
- return typeof ret === "string" ?
- // Handle most common string cases
- ret.replace(rreturn, "") :
- // Handle cases where value is null/undef or number
- ret == null ? "" : ret;
- }
-
- return;
- }
-
- isFunction = jQuery.isFunction( value );
-
- return this.each(function( i ) {
- var val;
-
- if ( this.nodeType !== 1 ) {
- return;
- }
-
- if ( isFunction ) {
- val = value.call( this, i, jQuery( this ).val() );
- } else {
- val = value;
- }
-
- // Treat null/undefined as ""; convert numbers to string
- if ( val == null ) {
- val = "";
-
- } else if ( typeof val === "number" ) {
- val += "";
-
- } else if ( jQuery.isArray( val ) ) {
- val = jQuery.map( val, function( value ) {
- return value == null ? "" : value + "";
- });
- }
-
- hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
-
- // If set returns undefined, fall back to normal setting
- if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
- this.value = val;
- }
- });
- }
-});
-
-jQuery.extend({
- valHooks: {
- option: {
- get: function( elem ) {
- var val = jQuery.find.attr( elem, "value" );
- return val != null ?
- val :
- // Support: IE10-11+
- // option.text throws exceptions (#14686, #14858)
- jQuery.trim( jQuery.text( elem ) );
- }
- },
- select: {
- get: function( elem ) {
- var value, option,
- options = elem.options,
- index = elem.selectedIndex,
- one = elem.type === "select-one" || index < 0,
- values = one ? null : [],
- max = one ? index + 1 : options.length,
- i = index < 0 ?
- max :
- one ? index : 0;
-
- // Loop through all the selected options
- for ( ; i < max; i++ ) {
- option = options[ i ];
-
- // IE6-9 doesn't update selected after form reset (#2551)
- if ( ( option.selected || i === index ) &&
- // Don't return options that are disabled or in a disabled optgroup
- ( support.optDisabled ? !option.disabled : option.getAttribute( "disabled" ) === null ) &&
- ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
-
- // Get the specific value for the option
- value = jQuery( option ).val();
-
- // We don't need an array for one selects
- if ( one ) {
- return value;
- }
-
- // Multi-Selects return an array
- values.push( value );
- }
- }
-
- return values;
- },
-
- set: function( elem, value ) {
- var optionSet, option,
- options = elem.options,
- values = jQuery.makeArray( value ),
- i = options.length;
-
- while ( i-- ) {
- option = options[ i ];
- if ( (option.selected = jQuery.inArray( option.value, values ) >= 0) ) {
- optionSet = true;
- }
- }
-
- // Force browsers to behave consistently when non-matching value is set
- if ( !optionSet ) {
- elem.selectedIndex = -1;
- }
- return values;
- }
- }
- }
-});
-
-// Radios and checkboxes getter/setter
-jQuery.each([ "radio", "checkbox" ], function() {
- jQuery.valHooks[ this ] = {
- set: function( elem, value ) {
- if ( jQuery.isArray( value ) ) {
- return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
- }
- }
- };
- if ( !support.checkOn ) {
- jQuery.valHooks[ this ].get = function( elem ) {
- return elem.getAttribute("value") === null ? "on" : elem.value;
- };
- }
-});
-
-
-
-
-// Return jQuery for attributes-only inclusion
-
-
-jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
- "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
- "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
-
- // Handle event binding
- jQuery.fn[ name ] = function( data, fn ) {
- return arguments.length > 0 ?
- this.on( name, null, data, fn ) :
- this.trigger( name );
- };
-});
-
-jQuery.fn.extend({
- hover: function( fnOver, fnOut ) {
- return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
- },
-
- bind: function( types, data, fn ) {
- return this.on( types, null, data, fn );
- },
- unbind: function( types, fn ) {
- return this.off( types, null, fn );
- },
-
- delegate: function( selector, types, data, fn ) {
- return this.on( types, selector, data, fn );
- },
- undelegate: function( selector, types, fn ) {
- // ( namespace ) or ( selector, types [, fn] )
- return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
- }
-});
-
-
-var nonce = jQuery.now();
-
-var rquery = (/\?/);
-
-
-
-// Support: Android 2.3
-// Workaround failure to string-cast null input
-jQuery.parseJSON = function( data ) {
- return JSON.parse( data + "" );
-};
-
-
-// Cross-browser xml parsing
-jQuery.parseXML = function( data ) {
- var xml, tmp;
- if ( !data || typeof data !== "string" ) {
- return null;
- }
-
- // Support: IE9
- try {
- tmp = new DOMParser();
- xml = tmp.parseFromString( data, "text/xml" );
- } catch ( e ) {
- xml = undefined;
- }
-
- if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
- jQuery.error( "Invalid XML: " + data );
- }
- return xml;
-};
-
-
-var
- rhash = /#.*$/,
- rts = /([?&])_=[^&]*/,
- rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
- // #7653, #8125, #8152: local protocol detection
- rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
- rnoContent = /^(?:GET|HEAD)$/,
- rprotocol = /^\/\//,
- rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,
-
- /* Prefilters
- * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
- * 2) These are called:
- * - BEFORE asking for a transport
- * - AFTER param serialization (s.data is a string if s.processData is true)
- * 3) key is the dataType
- * 4) the catchall symbol "*" can be used
- * 5) execution will start with transport dataType and THEN continue down to "*" if needed
- */
- prefilters = {},
-
- /* Transports bindings
- * 1) key is the dataType
- * 2) the catchall symbol "*" can be used
- * 3) selection will start with transport dataType and THEN go to "*" if needed
- */
- transports = {},
-
- // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
- allTypes = "*/".concat( "*" ),
-
- // Document location
- ajaxLocation = window.location.href,
-
- // Segment location into parts
- ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
-
-// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
-function addToPrefiltersOrTransports( structure ) {
-
- // dataTypeExpression is optional and defaults to "*"
- return function( dataTypeExpression, func ) {
-
- if ( typeof dataTypeExpression !== "string" ) {
- func = dataTypeExpression;
- dataTypeExpression = "*";
- }
-
- var dataType,
- i = 0,
- dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];
-
- if ( jQuery.isFunction( func ) ) {
- // For each dataType in the dataTypeExpression
- while ( (dataType = dataTypes[i++]) ) {
- // Prepend if requested
- if ( dataType[0] === "+" ) {
- dataType = dataType.slice( 1 ) || "*";
- (structure[ dataType ] = structure[ dataType ] || []).unshift( func );
-
- // Otherwise append
- } else {
- (structure[ dataType ] = structure[ dataType ] || []).push( func );
- }
- }
- }
- };
-}
-
-// Base inspection function for prefilters and transports
-function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
-
- var inspected = {},
- seekingTransport = ( structure === transports );
-
- function inspect( dataType ) {
- var selected;
- inspected[ dataType ] = true;
- jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
- var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
- if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
- options.dataTypes.unshift( dataTypeOrTransport );
- inspect( dataTypeOrTransport );
- return false;
- } else if ( seekingTransport ) {
- return !( selected = dataTypeOrTransport );
- }
- });
- return selected;
- }
-
- return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
-}
-
-// A special extend for ajax options
-// that takes "flat" options (not to be deep extended)
-// Fixes #9887
-function ajaxExtend( target, src ) {
- var key, deep,
- flatOptions = jQuery.ajaxSettings.flatOptions || {};
-
- for ( key in src ) {
- if ( src[ key ] !== undefined ) {
- ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];
- }
- }
- if ( deep ) {
- jQuery.extend( true, target, deep );
- }
-
- return target;
-}
-
-/* Handles responses to an ajax request:
- * - finds the right dataType (mediates between content-type and expected dataType)
- * - returns the corresponding response
- */
-function ajaxHandleResponses( s, jqXHR, responses ) {
-
- var ct, type, finalDataType, firstDataType,
- contents = s.contents,
- dataTypes = s.dataTypes;
-
- // Remove auto dataType and get content-type in the process
- while ( dataTypes[ 0 ] === "*" ) {
- dataTypes.shift();
- if ( ct === undefined ) {
- ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
- }
- }
-
- // Check if we're dealing with a known content-type
- if ( ct ) {
- for ( type in contents ) {
- if ( contents[ type ] && contents[ type ].test( ct ) ) {
- dataTypes.unshift( type );
- break;
- }
- }
- }
-
- // Check to see if we have a response for the expected dataType
- if ( dataTypes[ 0 ] in responses ) {
- finalDataType = dataTypes[ 0 ];
- } else {
- // Try convertible dataTypes
- for ( type in responses ) {
- if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
- finalDataType = type;
- break;
- }
- if ( !firstDataType ) {
- firstDataType = type;
- }
- }
- // Or just use first one
- finalDataType = finalDataType || firstDataType;
- }
-
- // If we found a dataType
- // We add the dataType to the list if needed
- // and return the corresponding response
- if ( finalDataType ) {
- if ( finalDataType !== dataTypes[ 0 ] ) {
- dataTypes.unshift( finalDataType );
- }
- return responses[ finalDataType ];
- }
-}
-
-/* Chain conversions given the request and the original response
- * Also sets the responseXXX fields on the jqXHR instance
- */
-function ajaxConvert( s, response, jqXHR, isSuccess ) {
- var conv2, current, conv, tmp, prev,
- converters = {},
- // Work with a copy of dataTypes in case we need to modify it for conversion
- dataTypes = s.dataTypes.slice();
-
- // Create converters map with lowercased keys
- if ( dataTypes[ 1 ] ) {
- for ( conv in s.converters ) {
- converters[ conv.toLowerCase() ] = s.converters[ conv ];
- }
- }
-
- current = dataTypes.shift();
-
- // Convert to each sequential dataType
- while ( current ) {
-
- if ( s.responseFields[ current ] ) {
- jqXHR[ s.responseFields[ current ] ] = response;
- }
-
- // Apply the dataFilter if provided
- if ( !prev && isSuccess && s.dataFilter ) {
- response = s.dataFilter( response, s.dataType );
- }
-
- prev = current;
- current = dataTypes.shift();
-
- if ( current ) {
-
- // There's only work to do if current dataType is non-auto
- if ( current === "*" ) {
-
- current = prev;
-
- // Convert response if prev dataType is non-auto and differs from current
- } else if ( prev !== "*" && prev !== current ) {
-
- // Seek a direct converter
- conv = converters[ prev + " " + current ] || converters[ "* " + current ];
-
- // If none found, seek a pair
- if ( !conv ) {
- for ( conv2 in converters ) {
-
- // If conv2 outputs current
- tmp = conv2.split( " " );
- if ( tmp[ 1 ] === current ) {
-
- // If prev can be converted to accepted input
- conv = converters[ prev + " " + tmp[ 0 ] ] ||
- converters[ "* " + tmp[ 0 ] ];
- if ( conv ) {
- // Condense equivalence converters
- if ( conv === true ) {
- conv = converters[ conv2 ];
-
- // Otherwise, insert the intermediate dataType
- } else if ( converters[ conv2 ] !== true ) {
- current = tmp[ 0 ];
- dataTypes.unshift( tmp[ 1 ] );
- }
- break;
- }
- }
- }
- }
-
- // Apply converter (if not an equivalence)
- if ( conv !== true ) {
-
- // Unless errors are allowed to bubble, catch and return them
- if ( conv && s[ "throws" ] ) {
- response = conv( response );
- } else {
- try {
- response = conv( response );
- } catch ( e ) {
- return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
- }
- }
- }
- }
- }
- }
-
- return { state: "success", data: response };
-}
-
-jQuery.extend({
-
- // Counter for holding the number of active queries
- active: 0,
-
- // Last-Modified header cache for next request
- lastModified: {},
- etag: {},
-
- ajaxSettings: {
- url: ajaxLocation,
- type: "GET",
- isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
- global: true,
- processData: true,
- async: true,
- contentType: "application/x-www-form-urlencoded; charset=UTF-8",
- /*
- timeout: 0,
- data: null,
- dataType: null,
- username: null,
- password: null,
- cache: null,
- throws: false,
- traditional: false,
- headers: {},
- */
-
- accepts: {
- "*": allTypes,
- text: "text/plain",
- html: "text/html",
- xml: "application/xml, text/xml",
- json: "application/json, text/javascript"
- },
-
- contents: {
- xml: /xml/,
- html: /html/,
- json: /json/
- },
-
- responseFields: {
- xml: "responseXML",
- text: "responseText",
- json: "responseJSON"
- },
-
- // Data converters
- // Keys separate source (or catchall "*") and destination types with a single space
- converters: {
-
- // Convert anything to text
- "* text": String,
-
- // Text to html (true = no transformation)
- "text html": true,
-
- // Evaluate text as a json expression
- "text json": jQuery.parseJSON,
-
- // Parse text as xml
- "text xml": jQuery.parseXML
- },
-
- // For options that shouldn't be deep extended:
- // you can add your own custom options here if
- // and when you create one that shouldn't be
- // deep extended (see ajaxExtend)
- flatOptions: {
- url: true,
- context: true
- }
- },
-
- // Creates a full fledged settings object into target
- // with both ajaxSettings and settings fields.
- // If target is omitted, writes into ajaxSettings.
- ajaxSetup: function( target, settings ) {
- return settings ?
-
- // Building a settings object
- ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
-
- // Extending ajaxSettings
- ajaxExtend( jQuery.ajaxSettings, target );
- },
-
- ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
- ajaxTransport: addToPrefiltersOrTransports( transports ),
-
- // Main method
- ajax: function( url, options ) {
-
- // If url is an object, simulate pre-1.5 signature
- if ( typeof url === "object" ) {
- options = url;
- url = undefined;
- }
-
- // Force options to be an object
- options = options || {};
-
- var transport,
- // URL without anti-cache param
- cacheURL,
- // Response headers
- responseHeadersString,
- responseHeaders,
- // timeout handle
- timeoutTimer,
- // Cross-domain detection vars
- parts,
- // To know if global events are to be dispatched
- fireGlobals,
- // Loop variable
- i,
- // Create the final options object
- s = jQuery.ajaxSetup( {}, options ),
- // Callbacks context
- callbackContext = s.context || s,
- // Context for global events is callbackContext if it is a DOM node or jQuery collection
- globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
- jQuery( callbackContext ) :
- jQuery.event,
- // Deferreds
- deferred = jQuery.Deferred(),
- completeDeferred = jQuery.Callbacks("once memory"),
- // Status-dependent callbacks
- statusCode = s.statusCode || {},
- // Headers (they are sent all at once)
- requestHeaders = {},
- requestHeadersNames = {},
- // The jqXHR state
- state = 0,
- // Default abort message
- strAbort = "canceled",
- // Fake xhr
- jqXHR = {
- readyState: 0,
-
- // Builds headers hashtable if needed
- getResponseHeader: function( key ) {
- var match;
- if ( state === 2 ) {
- if ( !responseHeaders ) {
- responseHeaders = {};
- while ( (match = rheaders.exec( responseHeadersString )) ) {
- responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
- }
- }
- match = responseHeaders[ key.toLowerCase() ];
- }
- return match == null ? null : match;
- },
-
- // Raw string
- getAllResponseHeaders: function() {
- return state === 2 ? responseHeadersString : null;
- },
-
- // Caches the header
- setRequestHeader: function( name, value ) {
- var lname = name.toLowerCase();
- if ( !state ) {
- name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
- requestHeaders[ name ] = value;
- }
- return this;
- },
-
- // Overrides response content-type header
- overrideMimeType: function( type ) {
- if ( !state ) {
- s.mimeType = type;
- }
- return this;
- },
-
- // Status-dependent callbacks
- statusCode: function( map ) {
- var code;
- if ( map ) {
- if ( state < 2 ) {
- for ( code in map ) {
- // Lazy-add the new callback in a way that preserves old ones
- statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
- }
- } else {
- // Execute the appropriate callbacks
- jqXHR.always( map[ jqXHR.status ] );
- }
- }
- return this;
- },
-
- // Cancel the request
- abort: function( statusText ) {
- var finalText = statusText || strAbort;
- if ( transport ) {
- transport.abort( finalText );
- }
- done( 0, finalText );
- return this;
- }
- };
-
- // Attach deferreds
- deferred.promise( jqXHR ).complete = completeDeferred.add;
- jqXHR.success = jqXHR.done;
- jqXHR.error = jqXHR.fail;
-
- // Remove hash character (#7531: and string promotion)
- // Add protocol if not provided (prefilters might expect it)
- // Handle falsy url in the settings object (#10093: consistency with old signature)
- // We also use the url parameter if available
- s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" )
- .replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
-
- // Alias method option to type as per ticket #12004
- s.type = options.method || options.type || s.method || s.type;
-
- // Extract dataTypes list
- s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ];
-
- // A cross-domain request is in order when we have a protocol:host:port mismatch
- if ( s.crossDomain == null ) {
- parts = rurl.exec( s.url.toLowerCase() );
- s.crossDomain = !!( parts &&
- ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
- ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !==
- ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) )
- );
- }
-
- // Convert data if not already a string
- if ( s.data && s.processData && typeof s.data !== "string" ) {
- s.data = jQuery.param( s.data, s.traditional );
- }
-
- // Apply prefilters
- inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
-
- // If request was aborted inside a prefilter, stop there
- if ( state === 2 ) {
- return jqXHR;
- }
-
- // We can fire global events as of now if asked to
- // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
- fireGlobals = jQuery.event && s.global;
-
- // Watch for a new set of requests
- if ( fireGlobals && jQuery.active++ === 0 ) {
- jQuery.event.trigger("ajaxStart");
- }
-
- // Uppercase the type
- s.type = s.type.toUpperCase();
-
- // Determine if request has content
- s.hasContent = !rnoContent.test( s.type );
-
- // Save the URL in case we're toying with the If-Modified-Since
- // and/or If-None-Match header later on
- cacheURL = s.url;
-
- // More options handling for requests with no content
- if ( !s.hasContent ) {
-
- // If data is available, append data to url
- if ( s.data ) {
- cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
- // #9682: remove data so that it's not used in an eventual retry
- delete s.data;
- }
-
- // Add anti-cache in url if needed
- if ( s.cache === false ) {
- s.url = rts.test( cacheURL ) ?
-
- // If there is already a '_' parameter, set its value
- cacheURL.replace( rts, "$1_=" + nonce++ ) :
-
- // Otherwise add one to the end
- cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
- }
- }
-
- // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
- if ( s.ifModified ) {
- if ( jQuery.lastModified[ cacheURL ] ) {
- jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
- }
- if ( jQuery.etag[ cacheURL ] ) {
- jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
- }
- }
-
- // Set the correct header, if data is being sent
- if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
- jqXHR.setRequestHeader( "Content-Type", s.contentType );
- }
-
- // Set the Accepts header for the server, depending on the dataType
- jqXHR.setRequestHeader(
- "Accept",
- s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
- s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
- s.accepts[ "*" ]
- );
-
- // Check for headers option
- for ( i in s.headers ) {
- jqXHR.setRequestHeader( i, s.headers[ i ] );
- }
-
- // Allow custom headers/mimetypes and early abort
- if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
- // Abort if not done already and return
- return jqXHR.abort();
- }
-
- // Aborting is no longer a cancellation
- strAbort = "abort";
-
- // Install callbacks on deferreds
- for ( i in { success: 1, error: 1, complete: 1 } ) {
- jqXHR[ i ]( s[ i ] );
- }
-
- // Get transport
- transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
-
- // If no transport, we auto-abort
- if ( !transport ) {
- done( -1, "No Transport" );
- } else {
- jqXHR.readyState = 1;
-
- // Send global event
- if ( fireGlobals ) {
- globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
- }
- // Timeout
- if ( s.async && s.timeout > 0 ) {
- timeoutTimer = setTimeout(function() {
- jqXHR.abort("timeout");
- }, s.timeout );
- }
-
- try {
- state = 1;
- transport.send( requestHeaders, done );
- } catch ( e ) {
- // Propagate exception as error if not done
- if ( state < 2 ) {
- done( -1, e );
- // Simply rethrow otherwise
- } else {
- throw e;
- }
- }
- }
-
- // Callback for when everything is done
- function done( status, nativeStatusText, responses, headers ) {
- var isSuccess, success, error, response, modified,
- statusText = nativeStatusText;
-
- // Called once
- if ( state === 2 ) {
- return;
- }
-
- // State is "done" now
- state = 2;
-
- // Clear timeout if it exists
- if ( timeoutTimer ) {
- clearTimeout( timeoutTimer );
- }
-
- // Dereference transport for early garbage collection
- // (no matter how long the jqXHR object will be used)
- transport = undefined;
-
- // Cache response headers
- responseHeadersString = headers || "";
-
- // Set readyState
- jqXHR.readyState = status > 0 ? 4 : 0;
-
- // Determine if successful
- isSuccess = status >= 200 && status < 300 || status === 304;
-
- // Get response data
- if ( responses ) {
- response = ajaxHandleResponses( s, jqXHR, responses );
- }
-
- // Convert no matter what (that way responseXXX fields are always set)
- response = ajaxConvert( s, response, jqXHR, isSuccess );
-
- // If successful, handle type chaining
- if ( isSuccess ) {
-
- // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
- if ( s.ifModified ) {
- modified = jqXHR.getResponseHeader("Last-Modified");
- if ( modified ) {
- jQuery.lastModified[ cacheURL ] = modified;
- }
- modified = jqXHR.getResponseHeader("etag");
- if ( modified ) {
- jQuery.etag[ cacheURL ] = modified;
- }
- }
-
- // if no content
- if ( status === 204 || s.type === "HEAD" ) {
- statusText = "nocontent";
-
- // if not modified
- } else if ( status === 304 ) {
- statusText = "notmodified";
-
- // If we have data, let's convert it
- } else {
- statusText = response.state;
- success = response.data;
- error = response.error;
- isSuccess = !error;
- }
- } else {
- // Extract error from statusText and normalize for non-aborts
- error = statusText;
- if ( status || !statusText ) {
- statusText = "error";
- if ( status < 0 ) {
- status = 0;
- }
- }
- }
-
- // Set data for the fake xhr object
- jqXHR.status = status;
- jqXHR.statusText = ( nativeStatusText || statusText ) + "";
-
- // Success/Error
- if ( isSuccess ) {
- deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
- } else {
- deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
- }
-
- // Status-dependent callbacks
- jqXHR.statusCode( statusCode );
- statusCode = undefined;
-
- if ( fireGlobals ) {
- globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
- [ jqXHR, s, isSuccess ? success : error ] );
- }
-
- // Complete
- completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
-
- if ( fireGlobals ) {
- globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
- // Handle the global AJAX counter
- if ( !( --jQuery.active ) ) {
- jQuery.event.trigger("ajaxStop");
- }
- }
- }
-
- return jqXHR;
- },
-
- getJSON: function( url, data, callback ) {
- return jQuery.get( url, data, callback, "json" );
- },
-
- getScript: function( url, callback ) {
- return jQuery.get( url, undefined, callback, "script" );
- }
-});
-
-jQuery.each( [ "get", "post" ], function( i, method ) {
- jQuery[ method ] = function( url, data, callback, type ) {
- // Shift arguments if data argument was omitted
- if ( jQuery.isFunction( data ) ) {
- type = type || callback;
- callback = data;
- data = undefined;
- }
-
- return jQuery.ajax({
- url: url,
- type: method,
- dataType: type,
- data: data,
- success: callback
- });
- };
-});
-
-
-jQuery._evalUrl = function( url ) {
- return jQuery.ajax({
- url: url,
- type: "GET",
- dataType: "script",
- async: false,
- global: false,
- "throws": true
- });
-};
-
-
-jQuery.fn.extend({
- wrapAll: function( html ) {
- var wrap;
-
- if ( jQuery.isFunction( html ) ) {
- return this.each(function( i ) {
- jQuery( this ).wrapAll( html.call(this, i) );
- });
- }
-
- if ( this[ 0 ] ) {
-
- // The elements to wrap the target around
- wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
-
- if ( this[ 0 ].parentNode ) {
- wrap.insertBefore( this[ 0 ] );
- }
-
- wrap.map(function() {
- var elem = this;
-
- while ( elem.firstElementChild ) {
- elem = elem.firstElementChild;
- }
-
- return elem;
- }).append( this );
- }
-
- return this;
- },
-
- wrapInner: function( html ) {
- if ( jQuery.isFunction( html ) ) {
- return this.each(function( i ) {
- jQuery( this ).wrapInner( html.call(this, i) );
- });
- }
-
- return this.each(function() {
- var self = jQuery( this ),
- contents = self.contents();
-
- if ( contents.length ) {
- contents.wrapAll( html );
-
- } else {
- self.append( html );
- }
- });
- },
-
- wrap: function( html ) {
- var isFunction = jQuery.isFunction( html );
-
- return this.each(function( i ) {
- jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
- });
- },
-
- unwrap: function() {
- return this.parent().each(function() {
- if ( !jQuery.nodeName( this, "body" ) ) {
- jQuery( this ).replaceWith( this.childNodes );
- }
- }).end();
- }
-});
-
-
-jQuery.expr.filters.hidden = function( elem ) {
- // Support: Opera <= 12.12
- // Opera reports offsetWidths and offsetHeights less than zero on some elements
- return elem.offsetWidth <= 0 && elem.offsetHeight <= 0;
-};
-jQuery.expr.filters.visible = function( elem ) {
- return !jQuery.expr.filters.hidden( elem );
-};
-
-
-
-
-var r20 = /%20/g,
- rbracket = /\[\]$/,
- rCRLF = /\r?\n/g,
- rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
- rsubmittable = /^(?:input|select|textarea|keygen)/i;
-
-function buildParams( prefix, obj, traditional, add ) {
- var name;
-
- if ( jQuery.isArray( obj ) ) {
- // Serialize array item.
- jQuery.each( obj, function( i, v ) {
- if ( traditional || rbracket.test( prefix ) ) {
- // Treat each array item as a scalar.
- add( prefix, v );
-
- } else {
- // Item is non-scalar (array or object), encode its numeric index.
- buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
- }
- });
-
- } else if ( !traditional && jQuery.type( obj ) === "object" ) {
- // Serialize object item.
- for ( name in obj ) {
- buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
- }
-
- } else {
- // Serialize scalar item.
- add( prefix, obj );
- }
-}
-
-// Serialize an array of form elements or a set of
-// key/values into a query string
-jQuery.param = function( a, traditional ) {
- var prefix,
- s = [],
- add = function( key, value ) {
- // If value is a function, invoke it and return its value
- value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
- s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
- };
-
- // Set traditional to true for jQuery <= 1.3.2 behavior.
- if ( traditional === undefined ) {
- traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
- }
-
- // If an array was passed in, assume that it is an array of form elements.
- if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
- // Serialize the form elements
- jQuery.each( a, function() {
- add( this.name, this.value );
- });
-
- } else {
- // If traditional, encode the "old" way (the way 1.3.2 or older
- // did it), otherwise encode params recursively.
- for ( prefix in a ) {
- buildParams( prefix, a[ prefix ], traditional, add );
- }
- }
-
- // Return the resulting serialization
- return s.join( "&" ).replace( r20, "+" );
-};
-
-jQuery.fn.extend({
- serialize: function() {
- return jQuery.param( this.serializeArray() );
- },
- serializeArray: function() {
- return this.map(function() {
- // Can add propHook for "elements" to filter or add form elements
- var elements = jQuery.prop( this, "elements" );
- return elements ? jQuery.makeArray( elements ) : this;
- })
- .filter(function() {
- var type = this.type;
-
- // Use .is( ":disabled" ) so that fieldset[disabled] works
- return this.name && !jQuery( this ).is( ":disabled" ) &&
- rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
- ( this.checked || !rcheckableType.test( type ) );
- })
- .map(function( i, elem ) {
- var val = jQuery( this ).val();
-
- return val == null ?
- null :
- jQuery.isArray( val ) ?
- jQuery.map( val, function( val ) {
- return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
- }) :
- { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
- }).get();
- }
-});
-
-
-jQuery.ajaxSettings.xhr = function() {
- try {
- return new XMLHttpRequest();
- } catch( e ) {}
-};
-
-var xhrId = 0,
- xhrCallbacks = {},
- xhrSuccessStatus = {
- // file protocol always yields status code 0, assume 200
- 0: 200,
- // Support: IE9
- // #1450: sometimes IE returns 1223 when it should be 204
- 1223: 204
- },
- xhrSupported = jQuery.ajaxSettings.xhr();
-
-// Support: IE9
-// Open requests must be manually aborted on unload (#5280)
-// See https://support.microsoft.com/kb/2856746 for more info
-if ( window.attachEvent ) {
- window.attachEvent( "onunload", function() {
- for ( var key in xhrCallbacks ) {
- xhrCallbacks[ key ]();
- }
- });
-}
-
-support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
-support.ajax = xhrSupported = !!xhrSupported;
-
-jQuery.ajaxTransport(function( options ) {
- var callback;
-
- // Cross domain only allowed if supported through XMLHttpRequest
- if ( support.cors || xhrSupported && !options.crossDomain ) {
- return {
- send: function( headers, complete ) {
- var i,
- xhr = options.xhr(),
- id = ++xhrId;
-
- xhr.open( options.type, options.url, options.async, options.username, options.password );
-
- // Apply custom fields if provided
- if ( options.xhrFields ) {
- for ( i in options.xhrFields ) {
- xhr[ i ] = options.xhrFields[ i ];
- }
- }
-
- // Override mime type if needed
- if ( options.mimeType && xhr.overrideMimeType ) {
- xhr.overrideMimeType( options.mimeType );
- }
-
- // X-Requested-With header
- // For cross-domain requests, seeing as conditions for a preflight are
- // akin to a jigsaw puzzle, we simply never set it to be sure.
- // (it can always be set on a per-request basis or even using ajaxSetup)
- // For same-domain requests, won't change header if already provided.
- if ( !options.crossDomain && !headers["X-Requested-With"] ) {
- headers["X-Requested-With"] = "XMLHttpRequest";
- }
-
- // Set headers
- for ( i in headers ) {
- xhr.setRequestHeader( i, headers[ i ] );
- }
-
- // Callback
- callback = function( type ) {
- return function() {
- if ( callback ) {
- delete xhrCallbacks[ id ];
- callback = xhr.onload = xhr.onerror = null;
-
- if ( type === "abort" ) {
- xhr.abort();
- } else if ( type === "error" ) {
- complete(
- // file: protocol always yields status 0; see #8605, #14207
- xhr.status,
- xhr.statusText
- );
- } else {
- complete(
- xhrSuccessStatus[ xhr.status ] || xhr.status,
- xhr.statusText,
- // Support: IE9
- // Accessing binary-data responseText throws an exception
- // (#11426)
- typeof xhr.responseText === "string" ? {
- text: xhr.responseText
- } : undefined,
- xhr.getAllResponseHeaders()
- );
- }
- }
- };
- };
-
- // Listen to events
- xhr.onload = callback();
- xhr.onerror = callback("error");
-
- // Create the abort callback
- callback = xhrCallbacks[ id ] = callback("abort");
-
- try {
- // Do send the request (this may raise an exception)
- xhr.send( options.hasContent && options.data || null );
- } catch ( e ) {
- // #14683: Only rethrow if this hasn't been notified as an error yet
- if ( callback ) {
- throw e;
- }
- }
- },
-
- abort: function() {
- if ( callback ) {
- callback();
- }
- }
- };
- }
-});
-
-
-
-
-// Install script dataType
-jQuery.ajaxSetup({
- accepts: {
- script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
- },
- contents: {
- script: /(?:java|ecma)script/
- },
- converters: {
- "text script": function( text ) {
- jQuery.globalEval( text );
- return text;
- }
- }
-});
-
-// Handle cache's special case and crossDomain
-jQuery.ajaxPrefilter( "script", function( s ) {
- if ( s.cache === undefined ) {
- s.cache = false;
- }
- if ( s.crossDomain ) {
- s.type = "GET";
- }
-});
-
-// Bind script tag hack transport
-jQuery.ajaxTransport( "script", function( s ) {
- // This transport only deals with cross domain requests
- if ( s.crossDomain ) {
- var script, callback;
- return {
- send: function( _, complete ) {
- script = jQuery("
-{% for resource in plugins.resources.js %}
- {% if resource.url %}
-
- {% else %}
-
- {% endif %}
-{% endfor %}
-
-{% endblock %}
-
-{% block style %}
-
- {% for resource in plugins.resources.css %}
- {% if resource.url %}
-
- {% else %}
-
- {% endif %}
- {% endfor %}
- {% for type, style in styles %}
-
- {% endfor %}
-{% endblock %}
From 3bc1934966bef98c69e6dc6d237364473202461f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:07:19 +0100
Subject: [PATCH 077/217] Add faq in documentation
---
docs/README.md | 3 +++
docs/faq.md | 8 ++++++++
2 files changed, 11 insertions(+)
create mode 100644 docs/faq.md
diff --git a/docs/README.md b/docs/README.md
index 98246f38f..2dfffba31 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,3 +1,6 @@
# GitBook v{{ book.version }}: Documentation
+### FAQ
+
+There are questions that are asked quite often, [check this out before creating an issue](faq.md).
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 000000000..592fbdc3b
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,8 @@
+# GitBook FAQ
+
+#### Should I use an `.html` or `.md` extensions in my links?
+
+You should always use `.md` extensions for your relative links, GitBook will automatically replace these links by the right value when the pointing file is referenced in the Table of Contents.
+
+
+
From 067bdcda2db081a6df55913b8502a94f1f8251ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:07:56 +0100
Subject: [PATCH 078/217] Add "config" to templating context
---
docs/variables.md | 4 ++--
lib/backbone/summary.js | 18 ++++++++++++++----
lib/book.js | 6 +++++-
lib/config/index.js | 7 +++++++
lib/output/base.js | 3 ++-
5 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/docs/variables.md b/docs/variables.md
index bf2e88d14..fb854638f 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -11,13 +11,12 @@ The following is a reference of the available data during book's parsing and the
| `page` | Current page specific information |
| `file` | File associated with the current page specific information |
| `summary` | Information about the table of contents |
+| `config` | Dump of the `book.json` |
### Book Variables
| Variable | Description |
| -------- | ----------- |
-| `book.title` | Title specified in the `book.json` (or detected from the README) |
-| `book.description` | Description specified in the `book.json` (or detected from the README) |
| `book.[CONFIGURATION_DATA]` | All the `variables` set via the `book.json` are available through the book variable. |
### GitBook Variables
@@ -33,6 +32,7 @@ The following is a reference of the available data during book's parsing and the
| -------- | ----------- |
| `file.path` | The path to the raw page |
| `file.mtime` | Modified Time, Time when file data last modified |
+| `file.type` | The name of the parser used to compile this file (ex: `markdown`, `asciidoc`, etc) |
#### Page Variables
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 5d4d53cd5..a1caae7fe 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -24,8 +24,12 @@ function TOCArticle(summary, title, ref, articles, parent) {
}
var parts = url.parse(ref);
- this.path = parts.pathname;
- this.anchor = parts.hash;
+ this.ref = ref;
+
+ if (!this.isExternal()) {
+ this.path = parts.pathname;
+ this.anchor = parts.hash;
+ }
this.articles = _.map(articles || [], function(article) {
if (article instanceof TOCArticle) return article;
@@ -45,8 +49,9 @@ TOCArticle.prototype.walk = function(iter) {
TOCArticle.prototype.getContext = function() {
return {
title: this.title,
- path: this.path,
- anchor: this.anchor
+ path: this.isExternal()? undefined : this.path,
+ anchor: this.isExternal()? undefined : this.anchor,
+ url: this.isExternal()? this.ref : undefined
};
};
@@ -55,6 +60,11 @@ 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 has children
TOCArticle.prototype.hasChildren = function() {
return this.articles.length > 0;
diff --git a/lib/book.js b/lib/book.js
index 7928f248c..d0e415c11 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -99,7 +99,11 @@ Book.prototype.getContext = function() {
return {
book: _.extend({
title: this.config.get('title'),
- description: this.config.get('description')
+ description: this.config.get('description'),
+ language: this.config.get('language'),
+ isbn: this.config.get('isbn'),
+ author: this.config.get('author'),
+ direction: this.config.get('direction')
}, variables)
};
};
diff --git a/lib/config/index.js b/lib/config/index.js
index ede45ba79..a5a0d67bb 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -131,4 +131,11 @@ Config.prototype.dump = function() {
return _.cloneDeep(this.options);
};
+// Return templating context
+Config.prototype.getContext = function() {
+ return {
+ config: this.book.config.dump()
+ };
+};
+
module.exports = Config;
diff --git a/lib/output/base.js b/lib/output/base.js
index ad2d90d98..638ee250f 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -161,7 +161,8 @@ Output.prototype.getPageContext = function(page) {
page.getContext(),
gitbook.getContext(),
this.book.getContext(),
- this.book.summary.getContext()
+ this.book.summary.getContext(),
+ this.book.config.getContext()
);
};
From 1769ac2c2d93e3244774aee49ff83f6649e1ad1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:08:08 +0100
Subject: [PATCH 079/217] Don't output config in json format
---
lib/output/json.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lib/output/json.js b/lib/output/json.js
index 4fb3bbf67..5c9e1f596 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -19,6 +19,9 @@ JSONOutput.prototype.onPage = function(page) {
.then(function() {
var json = that.getPageContext(page);
+ // Delete some private properties
+ delete json.config;
+
return that.writeFile(
page.withExtension('.json'),
JSON.stringify(json, null, 4)
From 46f017e68bd919c365311a78d02252a451afdc04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:08:26 +0100
Subject: [PATCH 080/217] Add type to file object in templating context
---
lib/page/index.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/page/index.js b/lib/page/index.js
index 06b210ea3..6dcefa117 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -113,7 +113,8 @@ Page.prototype.getContext = function() {
return {
file: {
path: this.path,
- mtime: this.mtime
+ mtime: this.mtime,
+ type: this.type
},
page: {
title: article? article.title : null,
From 40cdc285b613949347f2909d8f7027fc51b0db7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:18:49 +0100
Subject: [PATCH 081/217] Don't build context in templating engine
---
lib/template/index.js | 20 --------------------
test/template.js | 5 ++---
2 files changed, 2 insertions(+), 23 deletions(-)
diff --git a/lib/template/index.js b/lib/template/index.js
index 826657212..bb8272c5b 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -6,7 +6,6 @@ var escapeStringRegexp = require('escape-string-regexp');
var Promise = require('../utils/promise');
var error = require('../utils/error');
-var gitbook = require('../gitbook');
var defaultBlocks = require('./blocks');
var Loader = require('./loader');
@@ -345,29 +344,10 @@ TemplateEngine.prototype.render = function(content, context, options) {
path: null
});
- // Setup context for the template
- context = _.extend({}, context, {
- // Variables from book.json
- book: this.book.config.get('variables'),
-
- // Complete book.json
- config: this.book.config.dump(),
-
- // infos about gitbook
- gitbook: {
- version: gitbook.version,
- generator: this.book.config.get('generator')
- }
- });
-
// Setup path and type
if (options.path) {
options.path = this.book.resolve(options.path);
}
- if (!context.type && options.path) {
- var parser = parsers.get(path.extname(options.path));
- context.type = parser? parser.name : null;
- }
// Replace shortcuts
content = this.applyShortcuts(options.type, content);
diff --git a/test/template.js b/test/template.js
index a76fe9377..6eb227869 100644
--- a/test/template.js
+++ b/test/template.js
@@ -1,5 +1,4 @@
var mock = require('./mock');
-var pkg = require('../package.json');
var Output = require('../lib/output/base');
describe('Template', function() {
@@ -19,8 +18,8 @@ describe('Template', function() {
});
it('should render with variable', function() {
- return output.template.renderString('Version is {{ gitbook.version }}')
- .should.be.fulfilledWith('Version is '+pkg.version);
+ return output.template.renderString('Hello {{ world }}', { world: 'World'})
+ .should.be.fulfilledWith('Hello World');
});
});
From b2af55ac63886cba2075043554ff78c787cbb9e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:53:37 +0100
Subject: [PATCH 082/217] Wrap error when loading plugin
---
lib/plugins/plugin.js | 14 +++++++++++---
lib/utils/error.js | 8 ++++++++
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 4ed619c9c..64ae75f3a 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -3,6 +3,7 @@ var path = require('path');
var resolve = require('resolve');
var Promise = require('../utils/promise');
+var error = require('../utils/error');
var gitbook = require('../gitbook');
var registry = require('./registry');
@@ -12,7 +13,7 @@ function BookPlugin(book, pluginId) {
this.id = pluginId;
this.npmId = registry.npmId(pluginId);
- this.baseDir;
+ this.root;
this.packageInfos = undefined;
this.content = undefined;
@@ -42,7 +43,7 @@ BookPlugin.prototype.load = function() {
try {
var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });
- that.baseDir = path.dirname(res);
+ that.root = path.dirname(res);
that.packageInfos = require(res);
} catch (err) {
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
@@ -55,7 +56,14 @@ BookPlugin.prototype.load = function() {
}
// Load plugin JS content
- that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
+ try {
+ that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
+ } catch(err) {
+ throw new error.PluginError(err, {
+ plugin: that.id
+ });
+ }
+
return true;
})
diff --git a/lib/utils/error.js b/lib/utils/error.js
index c1abebd68..53d822144 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -48,6 +48,13 @@ var TemplateError = WrappedError({
filename: null
});
+// Error for nunjucks templates
+var PluginError = WrappedError({
+ message: 'Error with plugin "{plugin}": {origMessage}',
+ type: 'plugin',
+ plugin: null
+});
+
// Deprecate methods/fields
function deprecateMethod(fn, msg) {
return deprecated.method(msg, log.warn.ln, fn);
@@ -66,6 +73,7 @@ module.exports = {
FileOutOfScopeError: FileOutOfScopeError,
TemplateError: TemplateError,
+ PluginError: PluginError,
deprecateMethod: deprecateMethod,
deprecateField: deprecateField
From 1db3f3deab86c69f31b1cf1454bb23315639025a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:58:35 +0100
Subject: [PATCH 083/217] Don't fail if plugin is "empty"
---
lib/plugins/plugin.js | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 64ae75f3a..0216a1875 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -7,6 +7,12 @@ var error = require('../utils/error');
var gitbook = require('../gitbook');
var registry = require('./registry');
+// Return true if an error is a "module not found"
+function isModuleNotFound(err) {
+ return err.message.indexOf('Cannot find module') >= 0;
+}
+
+
function BookPlugin(book, pluginId) {
this.book = book;
this.log = this.book.log;
@@ -47,7 +53,7 @@ BookPlugin.prototype.load = function() {
that.packageInfos = require(res);
} catch (err) {
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
- if (err.message.indexOf('Cannot find module') < 0) throw err;
+ if (!isModuleNotFound(err)) throw err;
that.packageInfos = undefined;
that.content = undefined;
@@ -59,9 +65,15 @@ BookPlugin.prototype.load = function() {
try {
that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
} catch(err) {
- throw new error.PluginError(err, {
- plugin: that.id
- });
+ // It's no big deal if the plugin doesn't have an "index.js"
+ // (For example: themes)
+ if (isModuleNotFound(err)) {
+ that.content = {};
+ } else {
+ throw new error.PluginError(err, {
+ plugin: that.id
+ });
+ }
}
return true;
From 5fdee2cb5e846303b5bd4f10552b1d60201c3cb7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 15:59:27 +0100
Subject: [PATCH 084/217] Add file website output tests
---
test/all.js | 4 ++--
test/output-website.js | 5 +++++
test/website-theme.js | 17 -----------------
3 files changed, 7 insertions(+), 19 deletions(-)
create mode 100644 test/output-website.js
delete mode 100644 test/website-theme.js
diff --git a/test/all.js b/test/all.js
index df367b24c..f8ba1534b 100644
--- a/test/all.js
+++ b/test/all.js
@@ -16,6 +16,6 @@ require('./conrefs');
require('./page');
// Output
-require('./output-json');
require('./assets-inliner');
-require('./website-theme');
+require('./output-json');
+require('./output-website');
diff --git a/test/output-website.js b/test/output-website.js
new file mode 100644
index 000000000..86c9d2b2b
--- /dev/null
+++ b/test/output-website.js
@@ -0,0 +1,5 @@
+
+describe('Website Output', function() {
+
+});
+
diff --git a/test/website-theme.js b/test/website-theme.js
deleted file mode 100644
index afbae5e29..000000000
--- a/test/website-theme.js
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-describe('Website Theming', function() {
-
-// Spec proposal:
-// - Plugins can "export" a theme
-// - Theme can be overrided in the book itself
-
-
-// Themes structures
-// - _layout/ (Folder containing all the templates)
-// - page.html (Template used for generating a page)
-// - _assets/ (Assets of the theme copied to the output)
-
-
-});
-
From 4e7cb9b23f9234fcbcdc1d151501c1a7a7ea0028 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 16:20:45 +0100
Subject: [PATCH 085/217] Load template engine for website output using theme
configuration
---
lib/cli/helper.js | 1 -
lib/config/default.js | 8 ++-
lib/config/plugins.js | 2 +-
lib/output/conrefs.js | 4 +-
lib/output/ebook.js | 9 +++-
lib/output/folder.js | 2 +-
lib/output/json.js | 1 -
lib/output/website.js | 98 +++++++++++++++++++++++++++++++++++++
lib/output/website/index.js | 14 ------
lib/output/website/theme.js | 6 ---
package.json | 2 +-
11 files changed, 113 insertions(+), 34 deletions(-)
create mode 100644 lib/output/website.js
delete mode 100644 lib/output/website/index.js
delete mode 100644 lib/output/website/theme.js
diff --git a/lib/cli/helper.js b/lib/cli/helper.js
index 61b9957d3..e8a049fc8 100644
--- a/lib/cli/helper.js
+++ b/lib/cli/helper.js
@@ -36,7 +36,6 @@ var FORMATS = {
// the root of the book is the first argument (or current directory)
function bookCmd(fn) {
return function(args, kwargs) {
-
var input = args[0] || process.cwd();
var book = new Book({
fs: new NodeFS(),
diff --git a/lib/config/default.js b/lib/config/default.js
index a35f1a526..1695e11ec 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -20,6 +20,9 @@ module.exports = {
'summary': 'SUMMARY.md'
},
+ // Theme for output
+ 'theme': 'default',
+
// CSS Styles
'styles': {
'website': 'styles/website.css',
@@ -39,11 +42,6 @@ module.exports = {
// Variables for templating
'variables': {},
- // Set another theme with your own layout
- // It's recommended to use plugins or add more options for default theme, though
- // See https://github.com/GitbookIO/gitbook/issues/209
- 'theme': path.resolve(__dirname, '../theme'),
-
// Links in template (null: default, false: remove, string: new value)
'links': {
// Custom links at top of sidebar
diff --git a/lib/config/plugins.js b/lib/config/plugins.js
index 43eafdfe6..c75cd55e9 100644
--- a/lib/config/plugins.js
+++ b/lib/config/plugins.js
@@ -1,7 +1,7 @@
var _ = require('lodash');
// Default plugins added to each books
-var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings'];
+var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings', 'theme-default'];
// Return true if a plugin is a default plugin
function isDefaultPlugin(name, version) {
diff --git a/lib/output/conrefs.js b/lib/output/conrefs.js
index f1cd1233b..af37de8de 100644
--- a/lib/output/conrefs.js
+++ b/lib/output/conrefs.js
@@ -1,7 +1,7 @@
var path = require('path');
var util = require('util');
-var FolderOutput = require('./folder')();
+var folderOutput = require('./folder');
var Git = require('../utils/git');
var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
@@ -11,7 +11,7 @@ Mixin for output to resolve git conrefs
*/
module.exports = function conrefsLoader(Base) {
- Base = Base || FolderOutput;
+ Base = folderOutput(Base);
function ConrefsLoader() {
Base.apply(this, arguments);
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index 010f3d741..7e7a66d60 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -1,9 +1,14 @@
+var util = require('util');
+
var WebsiteOutput = require('./website');
var assetsInliner = require('./assets-inliner');
-var EbookOutput = assetsInliner(WebsiteOutput);
+function EbookOutput() {
+ WebsiteOutput.apply(this, arguments);
+}
+util.inherits(EbookOutput, WebsiteOutput);
EbookOutput.prototype.name = 'ebook';
-module.exports = EbookOutput;
+module.exports = assetsInliner(EbookOutput);
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 05843cd46..5a5134686 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -34,7 +34,7 @@ module.exports = function folderOutput(Base) {
return Promise()
.then(function() {
- return Base.prototype.prepare.apply(that);
+ return FolderOutput.super_.prototype.prepare.apply(that);
})
// Cleanup output folder
diff --git a/lib/output/json.js b/lib/output/json.js
index 5c9e1f596..58055799a 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -1,4 +1,3 @@
-var _ = require('lodash');
var conrefsLoader = require('./conrefs');
var JSONOutput = conrefsLoader();
diff --git a/lib/output/website.js b/lib/output/website.js
new file mode 100644
index 000000000..41395e659
--- /dev/null
+++ b/lib/output/website.js
@@ -0,0 +1,98 @@
+var _ = require('lodash');
+var path = require('path');
+var util = require('util');
+var nunjucks = require('nunjucks');
+
+var Promise = require('../utils/promise');
+var conrefsLoader = require('./conrefs');
+var Output = require('./base');
+
+// Tranform a theme ID into a plugin
+function themeID(plugin) {
+ return 'theme-' + plugin;
+}
+
+function WebsiteOutput() {
+ Output.apply(this, arguments);
+
+ // Nunjucks environment
+ this.env;
+
+ // Plugin instance for the main theme
+ this.theme;
+
+ // Plugin instance for the default theme
+ this.defaultTheme;
+}
+util.inherits(WebsiteOutput, Output);
+
+// Name of the generator
+// It's being used as a prefix for templates
+WebsiteOutput.prototype.name = 'website';
+
+// Load and setup the theme
+WebsiteOutput.prototype.prepare = function() {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ return WebsiteOutput.super_.prototype.prepare.apply(that);
+ })
+
+ .then(function() {
+ var themeName = that.book.config.get('theme');
+ that.theme = that.plugins.get(themeID(themeName));
+ that.themeDefault = that.plugins.get(themeID('default'));
+
+ if (!that.theme) {
+ throw new Error('Theme "' + themeName + '" is not installed, add "' + themeID(themeName) + '" to your "book.json"');
+ }
+
+ var searchPaths = _.chain([
+ // The book itself can contains a "_layouts" folder
+ that.book.root,
+
+ // Installed plugin (it can be identical to themeDefault.root)
+ that.theme.root,
+
+ // Is default theme still installed
+ that.themeDefault? that.themeDefault.root : null
+ ])
+ .compact()
+ .uniq()
+ .value();
+
+ that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(searchPaths));
+ });
+};
+
+// Write a page (parsable file)
+WebsiteOutput.prototype.onPage = function(page) {
+ var that = this;
+
+ // Render the page template with the same context as the json output
+ return this.render('page', this.getPageContext(page))
+
+ // Write the HTML file
+ .then(function(html) {
+ return that.writeFile(
+ page.withExtension('.html'),
+ html
+ );
+ });
+};
+
+// ----- Utilities ----
+
+// Render a template using nunjucks
+// Templates are stored in `_layouts` folders
+WebsiteOutput.prototype.render = function(tpl, context) {
+ return Promise.nfcall(this.env.render.bind(this.env), this.templateName(tpl), context);
+};
+
+// Return a complete name for a template
+WebsiteOutput.prototype.templateName = function(name) {
+ return path.join('_layouts', this.name, name+'.html');
+};
+
+module.exports = conrefsLoader(WebsiteOutput);
diff --git a/lib/output/website/index.js b/lib/output/website/index.js
deleted file mode 100644
index 6166ccc46..000000000
--- a/lib/output/website/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-var conrefsLoader = require('../conrefs');
-
-var Theme = require('./theme');
-
-var WebsiteOutput = conrefsLoader();
-
-WebsiteOutput.prototype.name = 'website';
-
-// Write a page (parsable file)
-WebsiteOutput.prototype.onPage = function(page) {
-
-};
-
-module.exports = WebsiteOutput;
diff --git a/lib/output/website/theme.js b/lib/output/website/theme.js
deleted file mode 100644
index 1cc2891ae..000000000
--- a/lib/output/website/theme.js
+++ /dev/null
@@ -1,6 +0,0 @@
-
-function Theme() {
-
-}
-
-module.exports = Theme;
diff --git a/package.json b/package.json
index 84da7c04a..8ef7d0004 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gitbook",
- "version": "2.6.7",
+ "version": "3.0.0-pre.0",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
From dac00be7b5a4aefa9cc3f63e5d94e729269bdfb0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 16:51:21 +0100
Subject: [PATCH 086/217] Add some doc about theming
---
docs/SUMMARY.md | 3 +++
docs/config.md | 23 +++++++++++++++++++++++
docs/themes.md | 26 ++++++++++++++++++++++++++
3 files changed, 52 insertions(+)
create mode 100644 docs/config.md
create mode 100644 docs/themes.md
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index ad5b67ad8..58d67636a 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -1,6 +1,7 @@
# Summary
* [About this document](README.md)
+* [Configuration](config.md)
* [Pages and Summary](pages.md)
* [Assets](assets.md)
* [Glossary](glossary.md)
@@ -8,3 +9,5 @@
* [Content References](conrefs.md)
* [Variables](variables.md)
* [Filters](filters.md)
+* [Plugins](plugins.md)
+* [Theming](themes.md)
\ No newline at end of file
diff --git a/docs/config.md b/docs/config.md
new file mode 100644
index 000000000..750f88f5f
--- /dev/null
+++ b/docs/config.md
@@ -0,0 +1,23 @@
+# Configuration
+
+GitBook allows you to customize your book using a flexible configuration. These options are specified in a `book.json` file.
+
+### Configuration Settings
+
+| Variable | Description |
+| -------- | ----------- |
+| `title` | Title of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
+| `description` | Description of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
+| `author` | Name of the author. On GitBook.com this field is pre-filled. |
+| `isbn` | ISBN of the book |
+| `language` | ISO code of the book's language, default value is `en` |
+| `direction` | `rtl` or `ltr`, default value depends on the value of `language` |
+| `gitbook` | [SemVer](http://semver.org) condition to validate which GitBook version should be used |
+| `plugins` | List of plugins to load, See [the plugins section](#plugins) for more details |
+| `pluginsConfig` |Configuration for plugins, See [the plugins section](#plugins) for more details |
+
+
+### Plugins
+
+Plugins and their configurations are specified in the `book.json`.
+
diff --git a/docs/themes.md b/docs/themes.md
new file mode 100644
index 000000000..d92f0ac38
--- /dev/null
+++ b/docs/themes.md
@@ -0,0 +1,26 @@
+# Theming
+
+Since version 3.0.0, GitBook can be easily themed. Books are using by default the [gitbook-theme-default](https://github.com/GitbookIO/gitbook-theme-default).
+
+The theme to use is specified in the [book's configuration](config.md) using key `theme`.
+
+> **Caution**: Custom theming can block some plugins from working correctly.
+
+### Structure of a theme
+
+A theme is a folder containing templates and assets. All the templates are optionnal, since theme are always extending the default theme.
+
+| Folder | Description |
+| -------- | ----------- |
+| `_layouts` | Main folder containing all the templates |
+| `_layouts/website/page.html` | Template for a normal page |
+| `_layouts/ebook/page.html` | Template for a normal page during ebook generation (PDF< ePub, Mobi) |
+
+
+### Extend/Customize theme in a book
+
+Authors can extend the templates of a theme directly from the book source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in
+
+### Publish a theme
+
+Themes are published as plugins ([see related docs](plugins.md)) with a `theme-` prefix. For example the theme `awesome` will be loaded from `theme-awesome` plugin, and then from `gitbook-plugin-theme-awesome` NPM package.
From d22c1f57e505eda6afe80fb6fe3a6b787d4aa699 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 17:04:23 +0100
Subject: [PATCH 087/217] Generate index.html instead of README.html
---
lib/output/base.js | 25 ++++++++++++++++++++++++-
lib/output/website.js | 19 ++++++++++++++++++-
lib/page/index.js | 18 ------------------
3 files changed, 42 insertions(+), 20 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 638ee250f..a6cfa0d22 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -3,6 +3,7 @@ var Ignore = require('ignore');
var path = require('path');
var Promise = require('../utils/promise');
+var pathUtil = require('../utils/path');
var PluginsManager = require('../plugins');
var TemplateEngine = require('../template');
var gitbook = require('../gitbook');
@@ -30,6 +31,9 @@ function Output(book) {
this.ignore = Ignore();
}
+// Default extension for output
+Output.prototype.defaultExtension = '.html';
+
// Start the generation, for a parsed book
Output.prototype.generate = function() {
var that = this;
@@ -126,7 +130,7 @@ Output.prototype.finish = function() {
// Resolve an HTML link
Output.prototype.onRelativeLink = function(currentPage, href) {
var to = this.book.getPage(href);
- if (to) return to.outputPath();
+ if (to) return this.outputPath(to.path);
return href;
};
@@ -166,4 +170,23 @@ Output.prototype.getPageContext = function(page) {
);
};
+
+// Filename for output
+// READMEs are replaced by index.html
+Output.prototype.outputPath = function(filename, ext) {
+ ext = ext || this.defaultExtension;
+ var output;
+
+ if (
+ path.basename(filename, path.extname(filename)) == 'README' ||
+ output == this.book.readme.path
+ ) {
+ output = path.join(path.dirname(output), 'index'+ext);
+ } else {
+ output = pathUtil.setExtension(output, ext);
+ }
+
+ return output;
+};
+
module.exports = Output;
diff --git a/lib/output/website.js b/lib/output/website.js
index 41395e659..e25240c46 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -63,6 +63,23 @@ WebsiteOutput.prototype.prepare = function() {
.value();
that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(searchPaths));
+
+ that.env.addGlobal('__', function(s) {
+ // todo: i18n
+ return s;
+ });
+
+ // Transform an absolute path into a relative path
+ // using this.ctx.page.path
+ that.env.addFilter('resolveFile', function(s) {
+ // todo
+ return s;
+ });
+
+ // Transform a '.md' into a '.html' (README -> index)
+ that.env.addFilter('contentURL', function(s) {
+ return that.outputPath(s);
+ });
});
};
@@ -76,7 +93,7 @@ WebsiteOutput.prototype.onPage = function(page) {
// Write the HTML file
.then(function(html) {
return that.writeFile(
- page.withExtension('.html'),
+ that.outputPath(page.path),
html
);
});
diff --git a/lib/page/index.js b/lib/page/index.js
index 6dcefa117..c9cd0664a 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -45,24 +45,6 @@ Page.prototype.withExtension = function(ext) {
return pathUtil.setExtension(this.path, ext);
};
-// Filename for output
-// READMEs are replaced by index.html
-Page.prototype.outputPath = function(ext) {
- ext = ext || '.html';
- var output;
-
- if (
- path.basename(this.path, path.extname(this.path)) == 'README' ||
- output == this.book.readme.path
- ) {
- output = path.join(path.dirname(output), 'index'+ext);
- } else {
- output = pathUtil.setExtension(output, ext);
- }
-
- return output;
-};
-
// Resolve a filename relative to this page
// It returns a path relative to the book root folder
Page.prototype.resolveLocal = function() {
From 096bdaba7aa11d56e0356386faaecb189f3100d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 17:18:19 +0100
Subject: [PATCH 088/217] Fix #537: don't add index.html to links
---
lib/output/base.js | 17 ++++++++++++++++-
lib/output/website.js | 2 +-
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index a6cfa0d22..7b527da33 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -4,6 +4,7 @@ var path = require('path');
var Promise = require('../utils/promise');
var pathUtil = require('../utils/path');
+var location = require('../utils/location');
var PluginsManager = require('../plugins');
var TemplateEngine = require('../template');
var gitbook = require('../gitbook');
@@ -130,7 +131,9 @@ Output.prototype.finish = function() {
// Resolve an HTML link
Output.prototype.onRelativeLink = function(currentPage, href) {
var to = this.book.getPage(href);
- if (to) return this.outputPath(to.path);
+
+ // Replace by an .html link
+ if (to) href = this.outputUrl(to.path);
return href;
};
@@ -189,4 +192,16 @@ Output.prototype.outputPath = function(filename, ext) {
return output;
};
+// Filename for output
+// /test/index.html -> /test/
+Output.prototype.outputUrl = function(filename, ext) {
+ var href = this.outputPath(filename, ext);
+
+ if (path.basename(href) == 'index.html') {
+ href = path.dirname(href) + '/';
+ }
+
+ return location.normalize(href);
+};
+
module.exports = Output;
diff --git a/lib/output/website.js b/lib/output/website.js
index e25240c46..1ec0548ea 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -78,7 +78,7 @@ WebsiteOutput.prototype.prepare = function() {
// Transform a '.md' into a '.html' (README -> index)
that.env.addFilter('contentURL', function(s) {
- return that.outputPath(s);
+ return that.onRelativeLink(null, s);
});
});
};
From 9caa9c39e59aba47de1646081591ed4c826abfe6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 17:34:27 +0100
Subject: [PATCH 089/217] Use _layouts folders as base for templates
---
lib/output/website.js | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/lib/output/website.js b/lib/output/website.js
index 1ec0548ea..a208802cc 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -12,6 +12,11 @@ function themeID(plugin) {
return 'theme-' + plugin;
}
+// Directory for a theme with the templates
+function templatesPath(dir) {
+ return path.join(dir, '_layouts');
+}
+
function WebsiteOutput() {
Output.apply(this, arguments);
@@ -50,15 +55,16 @@ WebsiteOutput.prototype.prepare = function() {
var searchPaths = _.chain([
// The book itself can contains a "_layouts" folder
- that.book.root,
+ '_layouts',
// Installed plugin (it can be identical to themeDefault.root)
- that.theme.root,
+ '_layouts',
// Is default theme still installed
that.themeDefault? that.themeDefault.root : null
])
.compact()
+ .map(templatesPath)
.uniq()
.value();
@@ -87,8 +93,13 @@ WebsiteOutput.prototype.prepare = function() {
WebsiteOutput.prototype.onPage = function(page) {
var that = this;
+ // Parse the page
+ return page.toHTML(this)
+
// Render the page template with the same context as the json output
- return this.render('page', this.getPageContext(page))
+ .then(function() {
+ return that.render('page', that.getPageContext(page));
+ })
// Write the HTML file
.then(function(html) {
@@ -109,7 +120,7 @@ WebsiteOutput.prototype.render = function(tpl, context) {
// Return a complete name for a template
WebsiteOutput.prototype.templateName = function(name) {
- return path.join('_layouts', this.name, name+'.html');
+ return path.join(this.name, name+'.html');
};
module.exports = conrefsLoader(WebsiteOutput);
From 2b5e836a3329917f1af4c0de51974b672994ec4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Wed, 17 Feb 2016 17:38:39 +0100
Subject: [PATCH 090/217] Fix and improve tests for links in page
---
test/page.js | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/test/page.js b/test/page.js
index b94751999..52e6d4e58 100644
--- a/test/page.js
+++ b/test/page.js
@@ -7,7 +7,7 @@ describe('Page', function() {
before(function() {
return mock.setupDefaultBook({
'heading.md': '# Hello\n\n## World',
- 'links.md': '[link](hello.md) [readme](README.md)',
+ 'links.md': '[link](hello.md) [link 2](variables/page/next.md) [readme](README.md)',
'codes/simple.md': '```hello world```',
'codes/lang.md': '```js\nhello world\n```',
@@ -130,7 +130,15 @@ describe('Page', function() {
it('should replace links to page to .html', function() {
page.content.should.be.html({
- 'a[href="index.html"]': {
+ 'a[href="./"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should use directory urls when file is a README', function() {
+ page.content.should.be.html({
+ 'a[href="./"]': {
count: 1
}
});
From a68364d4d7593d43a83a1d16d71db32bde0a6465 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:02:42 +0100
Subject: [PATCH 091/217] Add test for location
---
test/all.js | 3 +++
test/location.js | 36 ++++++++++++++++++++++++++++++++++++
2 files changed, 39 insertions(+)
create mode 100644 test/location.js
diff --git a/test/all.js b/test/all.js
index f8ba1534b..97913d7f3 100644
--- a/test/all.js
+++ b/test/all.js
@@ -1,4 +1,7 @@
+// Utilities
+require('./location');
+
// Parsing
require('./config');
require('./readme');
diff --git a/test/location.js b/test/location.js
new file mode 100644
index 000000000..4d949facd
--- /dev/null
+++ b/test/location.js
@@ -0,0 +1,36 @@
+var location = require('../lib/utils/location');
+
+describe('Location', function() {
+ it('should correctly test external location', function() {
+ location.isExternal('http://google.fr').should.be.exactly(true);
+ location.isExternal('https://google.fr').should.be.exactly(true);
+ location.isExternal('test.md').should.be.exactly(false);
+ location.isExternal('folder/test.md').should.be.exactly(false);
+ location.isExternal('/folder/test.md').should.be.exactly(false);
+ });
+
+ it('should correctly detect anchor location', function() {
+ location.isAnchor('#test').should.be.exactly(true);
+ location.isAnchor(' #test').should.be.exactly(true);
+ location.isAnchor('https://google.fr#test').should.be.exactly(false);
+ location.isAnchor('test.md#test').should.be.exactly(false);
+ });
+
+ describe('toAbsolute', function() {
+ it('should correctly transform as absolute', function() {
+ location.toAbsolute('http://google.fr').should.be.equal('http://google.fr');
+ location.toAbsolute('test.md', './', './').should.be.equal('test.md');
+ location.toAbsolute('folder/test.md', './', './').should.be.equal('folder/test.md');
+ });
+
+ it('should correctly handle windows path', function() {
+ location.toAbsolute('folder\\test.md', './', './').should.be.equal('folder/test.md');
+ });
+
+ it('should correctly handle absolute path', function() {
+ location.toAbsolute('/test.md', './', './').should.be.equal('test.md');
+ location.toAbsolute('/test.md', 'test', 'test').should.be.equal('../test.md');
+ location.toAbsolute('/sub/test.md', 'test', 'test').should.be.equal('../sub/test.md');
+ });
+ });
+});
From e75002a96d00f608f75236344ceb4434c487d863 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:03:00 +0100
Subject: [PATCH 092/217] Improve annotation with glossary terms
---
lib/page/html.js | 112 ++++++++++++++++++++++++++++++++++++++++++----
lib/page/index.js | 18 +++++++-
test/page.js | 42 ++++++++++++++++-
3 files changed, 160 insertions(+), 12 deletions(-)
diff --git a/lib/page/html.js b/lib/page/html.js
index 71b29b2c3..afa293696 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -6,14 +6,8 @@ var slug = require('github-slugid');
var Promise = require('../utils/promise');
var location = require('../utils/location');
-// Render a cheerio DOM as html
-function renderDOM($, dom, options) {
- if (!dom && $._root && $._root.children) {
- dom = $._root.children;
- }
- options = options|| dom.options || $._options;
- return domSerializer(dom, options);
-}
+// Selector to ignore
+var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script';
function HTMLPipeline(htmlString, opts) {
_.bindAll(this);
@@ -29,7 +23,13 @@ function HTMLPipeline(htmlString, opts) {
onCodeBlock: _.identity,
// Output a svg, if returns null the svg is kept inlined
- onOutputSVG: _.constant(null)
+ onOutputSVG: _.constant(null),
+
+ // Words to annotate
+ annotations: [],
+
+ // When an annotation is applied
+ onAnnotation: function () { }
});
this.$ = cheerio.load(htmlString, {
@@ -146,6 +146,33 @@ HTMLPipeline.prototype.transformSvgs = function() {
});
};
+// Annotate the content
+HTMLPipeline.prototype.applyAnnotations = function() {
+ var that = this;
+
+ _.each(this.opts.annotations, function(annotation) {
+ var searchRegex = new RegExp( '\\b(' + pregQuote(annotation.name.toLowerCase()) + ')\\b' , 'gi' );
+
+ that.$('*').each(function() {
+ var $this = that.$(this);
+
+ if (
+ $this.is(ANNOTATION_IGNORE) ||
+ $this.parents(ANNOTATION_IGNORE).length > 0
+ ) return;
+
+ replaceText(that.$, this, searchRegex, function(match) {
+ that.opts.onAnnotation(annotation);
+
+ return ''
+ + match
+ + ' ';
+ });
+ });
+ });
+};
+
// Write content to the pipeline
HTMLPipeline.prototype.output = function() {
var that = this;
@@ -156,9 +183,76 @@ HTMLPipeline.prototype.output = function() {
.then(this.transformHeadings)
.then(this.transformCodeBlocks)
.then(this.transformSvgs)
+ .then(this.applyAnnotations)
.then(function() {
return renderDOM(that.$);
});
};
+
+// Render a cheerio DOM as html
+function renderDOM($, dom, options) {
+ if (!dom && $._root && $._root.children) {
+ dom = $._root.children;
+ }
+ options = options|| dom.options || $._options;
+ return domSerializer(dom, options);
+}
+
+// Replace text in an element
+function replaceText($, el, search, replace, text_only ) {
+ return $(el).each(function(){
+ var node = this.firstChild,
+ val,
+ new_val,
+
+ // Elements to be removed at the end.
+ remove = [];
+
+ // Only continue if firstChild exists.
+ if ( node ) {
+
+ // Loop over all childNodes.
+ while (node) {
+
+ // Only process text nodes.
+ if ( node.nodeType === 3 ) {
+
+ // The original node value.
+ val = node.nodeValue;
+
+ // The new value.
+ new_val = val.replace( search, replace );
+
+ // Only replace text if the new value is actually different!
+ if ( new_val !== val ) {
+
+ if ( !text_only && /\|\:])/g, '\\$1');
+}
+
module.exports = HTMLPipeline;
diff --git a/lib/page/index.js b/lib/page/index.js
index c9cd0664a..629bcee90 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -147,7 +147,11 @@ Page.prototype.toHTML = function(output) {
// Normalize HTML output
.then(function() {
var pipelineOpts = {
- onRelativeLink: _.partial(output.onRelativeLink, that),
+ onRelativeLink: function (href) {
+ href = that.relative(href);
+ return output.onRelativeLink(that, href);
+ },
+ //_.partial(output.onRelativeLink, that),
onImage: _.partial(output.onOutputImage, that),
onOutputSVG: _.partial(output.onOutputSVG, that),
@@ -159,7 +163,17 @@ Page.prototype.toHTML = function(output) {
language: lang
}
});
- }
+ },
+
+ // Convert glossary entries to annotations
+ annotations: _.map(that.book.glossary.entries, function(entry) {
+ return {
+ id: entry.id,
+ name: entry.name,
+ description: entry.description,
+ href: '/GLOSSARY.html'
+ };
+ })
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
diff --git a/test/page.js b/test/page.js
index 52e6d4e58..b68e894e9 100644
--- a/test/page.js
+++ b/test/page.js
@@ -9,6 +9,10 @@ describe('Page', function() {
'heading.md': '# Hello\n\n## World',
'links.md': '[link](hello.md) [link 2](variables/page/next.md) [readme](README.md)',
+ 'annotations/simple.md': 'A magicien say abracadabra!',
+ 'annotations/code.md': 'A magicien say `abracadabra`!',
+ 'annotations/class.md': 'A magicien say abracadabra , right?
!',
+
'codes/simple.md': '```hello world```',
'codes/lang.md': '```js\nhello world\n```',
'codes/lang.adoc': '```js\nhello world\n```',
@@ -19,7 +23,9 @@ describe('Page', function() {
'variables/file/path.md': '{{ file.path }}',
'variables/page/title.md': '{{ page.title }}',
'variables/page/previous.md': '{{ page.previous.title }} {{ page.previous.path }}',
- 'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}'
+ 'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}',
+
+ 'GLOSSARY.md': '# Glossary\n\n\n## abracadabra\n\nthis is the description'
}, [
{
title: 'Test page.next',
@@ -186,4 +192,38 @@ describe('Page', function() {
.should.be.fulfilledWith('Test Variables variables/page/title.md
\n');
});
});
+
+ describe('Annotations / Glossary', function() {
+ it('should replace glossary terms', function() {
+ return book.addPage('annotations/simple.md').toHTML(output)
+ .should.finally.be.html({
+ '.glossary-term': {
+ count: 1,
+ text: 'abracadabra',
+ attributes: {
+ title: 'this is the description',
+ href: '../GLOSSARY.html#abracadabra'
+ }
+ }
+ });
+ });
+
+ it('should not replace terms in code blocks', function() {
+ return book.addPage('annotations/code.md').toHTML(output)
+ .should.finally.be.html({
+ '.glossary-term': {
+ count: 0
+ }
+ });
+ });
+
+ it('should not replace terms in ".no-glossary"', function() {
+ return book.addPage('annotations/class.md').toHTML(output)
+ .should.finally.be.html({
+ '.glossary-term': {
+ count: 0
+ }
+ });
+ });
+ });
});
From 1ec8b7c32403c2252ea379c327349fcc937b57fc Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:25:08 +0100
Subject: [PATCH 093/217] Add glossary as a normal page
---
lib/book.js | 8 +++++++-
test/parse.js | 11 +++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/lib/book.js b/lib/book.js
index d0e415c11..acfaecea3 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -216,7 +216,13 @@ Book.prototype.parse = function() {
})
// Parse the glossary
- .then(that.glossary.load);
+ .then(that.glossary.load)
+
+ // Add the glossary as a page
+ .then(function() {
+ if (!that.glossary.exists()) return;
+ that.addPage(that.glossary.path);
+ });
});
};
diff --git a/test/parse.js b/test/parse.js
index 72e026054..e1901072b 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -19,6 +19,17 @@ describe('Parsing', function() {
});
});
+ it('should add GLOSSARY as a page', function() {
+ return mock.setupDefaultBook({
+ 'GLOSSARY.md': ''
+ })
+ .then(function(book) {
+ return book.parse()
+ .then(function() {
+ book.hasPage('GLOSSARY.md').should.equal(true);
+ });
+ });
+ });
describe('Multilingual book', function() {
var book;
From 5993e6c79143bfe9ba989d450f8aece366af7d82 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:25:53 +0100
Subject: [PATCH 094/217] Ignore headings for annotations
---
lib/page/html.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/page/html.js b/lib/page/html.js
index afa293696..d667059d9 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -7,7 +7,7 @@ var Promise = require('../utils/promise');
var location = require('../utils/location');
// Selector to ignore
-var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script';
+var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6';
function HTMLPipeline(htmlString, opts) {
_.bindAll(this);
From a78e52812f5086612fe4ba20c822fee30eb6eaa5 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:29:25 +0100
Subject: [PATCH 095/217] Remove write operations from virtual fs
---
lib/fs/index.js | 55 +------------------------------------------------
lib/fs/node.js | 13 ------------
2 files changed, 1 insertion(+), 67 deletions(-)
diff --git a/lib/fs/index.js b/lib/fs/index.js
index 3e0d711da..8a3ca1e3f 100644
--- a/lib/fs/index.js
+++ b/lib/fs/index.js
@@ -1,13 +1,10 @@
var _ = require('lodash');
var path = require('path');
-var Buffer = require('buffer').Buffer;
-var destroy = require('destroy');
-var os = require('os');
var Promise = require('../utils/promise');
/*
-A filesystem is an interface to read/write files
+A filesystem is an interface to read files
GitBook can works with a virtual filesystem, for example in the browser.
*/
@@ -36,11 +33,6 @@ FS.prototype.stat = function(filename) {
// To implement for each fs
};
-// Write a file and returns a promise
-FS.prototype.write = function(filename, buffer) {
- // To implement for each fs
-};
-
// List files/directories in a directory
FS.prototype.readdir = function(folder) {
// To implement for each fs
@@ -49,11 +41,6 @@ FS.prototype.readdir = function(folder) {
// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
// For optmization, it can be redefined:
-// Allocate a new temporary directory
-FS.prototype.tmpdir = function() {
- return path.resolve(os.tmpdir(), 'gitbook_tmp_'+Date.now());
-};
-
// List files in a directory
FS.prototype.listFiles = function(folder) {
return this.readdir(folder)
@@ -96,46 +83,6 @@ FS.prototype.readAsString = function(filename) {
});
};
-// Write a stream to a file and returns a promise
-FS.prototype.writeStream = function(filename, stream) {
- var bufs = [];
- var d = Promise.defer();
-
- var cleanup = function() {
- destroy(stream);
- stream.removeAllListeners();
- };
-
- stream.on('data', function(d) {
- bufs.push(d);
- });
-
- stream.on('error', function(err) {
- cleanup();
-
- d.reject(err);
- });
-
- stream.on('end', function(){
- cleanup();
-
- var buf = Buffer.concat(bufs);
- d.resolve(buf);
- });
-
- return d.promise;
-};
-
-// Copy a file
-FS.prototype.copy = function(from, to) {
- var that = this;
-
- return this.read(from)
- .then(function(buf) {
- return that.write(to, buf);
- });
-};
-
// Find a file in a folder (case incensitive)
// Return the real filename
FS.prototype.findFile = function findFile(root, filename) {
diff --git a/lib/fs/node.js b/lib/fs/node.js
index 330391a0a..fc2517e76 100644
--- a/lib/fs/node.js
+++ b/lib/fs/node.js
@@ -26,19 +26,6 @@ NodeFS.prototype.stat = function(filename) {
return fs.stat(filename);
};
-// Write a file and returns a promise
-NodeFS.prototype.write = function(filename, buffer) {
- var folder = path.dirname(filename);
- return Promise()
- .then(function() {
- if (!folder) return;
- return fs.mkdirp(folder);
- })
- .then(function() {
- return fs.writeFile(filename, buffer);
- });
-};
-
// List files in a directory
NodeFS.prototype.readdir = function(folder) {
return fs.readdir(folder)
From 3d1eac234a1633f4822fd019464434124875b7b1 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:35:25 +0100
Subject: [PATCH 096/217] Move comment about substack/node-resolve#81
---
lib/plugins/plugin.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 0216a1875..ae8886cbc 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -8,11 +8,11 @@ var gitbook = require('../gitbook');
var registry = require('./registry');
// Return true if an error is a "module not found"
+// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
function isModuleNotFound(err) {
return err.message.indexOf('Cannot find module') >= 0;
}
-
function BookPlugin(book, pluginId) {
this.book = book;
this.log = this.book.log;
@@ -52,7 +52,6 @@ BookPlugin.prototype.load = function() {
that.root = path.dirname(res);
that.packageInfos = require(res);
} catch (err) {
- // Wait on https://github.com/substack/node-resolve/pull/81 to be merged
if (!isModuleNotFound(err)) throw err;
that.packageInfos = undefined;
From 004ac8851622cb4583b5569ae3480344855a7880 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Wed, 17 Feb 2016 21:43:54 +0100
Subject: [PATCH 097/217] Add method .hook to plugins
---
lib/plugins/index.js | 6 ++++++
lib/plugins/plugin.js | 25 +++++++++++++++++++++++++
2 files changed, 31 insertions(+)
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 6eb788b96..343c569a2 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -95,7 +95,13 @@ PluginsManager.prototype.install = function() {
return registry.install(that.book, plugin.name, plugin.version);
})
.thenResolve(plugins.length);
+};
+// Call a hook on all plugins to transform an input
+PluginsManager.prototype.hook = function(name, input) {
+ return Promise.reduce(this.plugins, function(current, plugin) {
+ return plugin.hook(name, current);
+ }, input);
};
module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index ae8886cbc..9f9cc35de 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -7,6 +7,10 @@ var error = require('../utils/error');
var gitbook = require('../gitbook');
var registry = require('./registry');
+var HOOKS = [
+ 'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
+];
+
// Return true if an error is a "module not found"
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
function isModuleNotFound(err) {
@@ -109,4 +113,25 @@ BookPlugin.prototype.validate = function() {
}
};
+// Call a hook and returns its result
+BookPlugin.prototype.hook = function(name, input) {
+ // Our book will be the context to apply
+ var context = this.book;
+
+ var hookFunc = this.content.hooks? this.content.hooks[name] : null;
+ input = input || {};
+
+ if (!hookFunc) return Promise(input);
+
+ this.book.log.debug.ln('call hook "' + name + '" for plugin "' + this.id + '"');
+ if (!_.contains(HOOKS, name)) {
+ this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions');
+ }
+
+ return Promise()
+ .then(function() {
+ return hookFunc.apply(context, [input]);
+ });
+};
+
module.exports = BookPlugin;
From b66242db985fd8d6088b52d4b089f0de2ecb8058 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 10:04:23 +0100
Subject: [PATCH 098/217] Add filter resolveAsset to template for website
---
lib/output/assets-inliner.js | 2 +-
lib/output/base.js | 8 ++++++++
lib/output/folder.js | 5 ++++-
lib/output/website.js | 11 ++++++++---
lib/page/index.js | 1 -
package.json | 6 +-----
6 files changed, 22 insertions(+), 11 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index b5d076eac..8d7c48e79 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -68,7 +68,7 @@ module.exports = function assetsInliner(Base) {
}
// Convert SVG to PNG
- return that.convertSVGFile(that.resolveForPage(page, src));
+ return that.convertSVGFile(that.resolveOutputForPage(page, src));
})
// Return relative path from the page
diff --git a/lib/output/base.js b/lib/output/base.js
index 7b527da33..e674f4652 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -173,6 +173,14 @@ Output.prototype.getPageContext = function(page) {
);
};
+// Resolve a file path in the context of a specific page
+// Result is an "absolute path relative to the output folder"
+Output.prototype.resolveForPage = function(page, href) {
+ if (_.isString(page)) page = this.book.getPage(page);
+
+ href = page.relative(href);
+ return this.onRelativeLink(page, href);
+};
// Filename for output
// READMEs are replaced by index.html
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 5a5134686..14e3f227f 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -72,7 +72,10 @@ module.exports = function folderOutput(Base) {
};
// Resolve a file path from a page (in the output folder)
- FolderOutput.prototype.resolveForPage = function(page, filename) {
+ // Result is an absolute path (enforced in outptu folder)
+ FolderOutput.prototype.resolveOutputForPage = function(page, filename) {
+ if (_.isString(page)) page = this.book.getPage(page);
+
var abs = page.resolveLocal(filename);
return this.resolve(abs);
};
diff --git a/lib/output/website.js b/lib/output/website.js
index a208802cc..f58c68fb0 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -77,15 +77,20 @@ WebsiteOutput.prototype.prepare = function() {
// Transform an absolute path into a relative path
// using this.ctx.page.path
- that.env.addFilter('resolveFile', function(s) {
- // todo
- return s;
+ that.env.addFilter('resolveFile', function(href) {
+ return that.resolveForPage(this.ctx.file.path, href);
});
// Transform a '.md' into a '.html' (README -> index)
that.env.addFilter('contentURL', function(s) {
return that.onRelativeLink(null, s);
});
+
+ // Relase path to an asset
+ that.env.addFilter('resolveAsset', function(href) {
+ href = path.join('/gitbook', href);
+ return that.resolveForPage(this.ctx.file.path, href);
+ });
});
};
diff --git a/lib/page/index.js b/lib/page/index.js
index 629bcee90..c92bdfa20 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -151,7 +151,6 @@ Page.prototype.toHTML = function(output) {
href = that.relative(href);
return output.onRelativeLink(that, href);
},
- //_.partial(output.onRelativeLink, that),
onImage: _.partial(output.onOutputImage, that),
onOutputSVG: _.partial(output.onOutputSVG, that),
diff --git a/package.json b/package.json
index 8ef7d0004..03bc3d162 100644
--- a/package.json
+++ b/package.json
@@ -59,11 +59,7 @@
"gulp-util": "3.0.6",
"browserify": "11.0.1",
"merge-stream": "0.1.7",
- "vinyl-source-stream": "1.1.0",
- "jquery": "2.1.4",
- "mousetrap": "1.5.3",
- "font-awesome": "4.1.0",
- "gitbook-markdown-css": "1.0.1"
+ "vinyl-source-stream": "1.1.0"
},
"scripts": {
"test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js"
From 1de9d5683b30b4f24c2aba4f796a6d77f0ce7004 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 10:45:50 +0100
Subject: [PATCH 099/217] Copy assets to output (website)
---
lib/output/website.js | 45 ++++++++++++++++++++++++++++++++++++++-----
lib/utils/fs.js | 13 ++++---------
package.json | 5 +++--
3 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/lib/output/website.js b/lib/output/website.js
index f58c68fb0..b59589772 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -4,6 +4,7 @@ var util = require('util');
var nunjucks = require('nunjucks');
var Promise = require('../utils/promise');
+var fs = require('../utils/fs');
var conrefsLoader = require('./conrefs');
var Output = require('./base');
@@ -17,7 +18,7 @@ function templatesPath(dir) {
return path.join(dir, '_layouts');
}
-function WebsiteOutput() {
+function _WebsiteOutput() {
Output.apply(this, arguments);
// Nunjucks environment
@@ -29,7 +30,9 @@ function WebsiteOutput() {
// Plugin instance for the default theme
this.defaultTheme;
}
-util.inherits(WebsiteOutput, Output);
+util.inherits(_WebsiteOutput, Output);
+
+var WebsiteOutput = conrefsLoader(_WebsiteOutput);
// Name of the generator
// It's being used as a prefix for templates
@@ -53,12 +56,13 @@ WebsiteOutput.prototype.prepare = function() {
throw new Error('Theme "' + themeName + '" is not installed, add "' + themeID(themeName) + '" to your "book.json"');
}
+ // This list is ordered to give priority to templates in the book
var searchPaths = _.chain([
// The book itself can contains a "_layouts" folder
- '_layouts',
+ that.book.root,
// Installed plugin (it can be identical to themeDefault.root)
- '_layouts',
+ that.theme.root,
// Is default theme still installed
that.themeDefault? that.themeDefault.root : null
@@ -91,6 +95,37 @@ WebsiteOutput.prototype.prepare = function() {
href = path.join('/gitbook', href);
return that.resolveForPage(this.ctx.file.path, href);
});
+ })
+
+ // Copy assets before copyign files from book
+ .then(function() {
+ return Promise.serie([
+ // Assets from the book are already copied
+ // The order is reversed from the template's one
+
+ // Is default theme still installed
+ that.themeDefault && that.themeDefault.root != that.theme.root?
+ that.themeDefault.root : null,
+
+ // Installed plugin (it can be identical to themeDefault.root)
+ that.theme.root
+ ], function(folder) {
+ if (!folder) return;
+
+ // Copy assets only if exists (don't fail otherwise)
+ var assetFolder = path.join(folder, '_assets', that.name);
+ if (!fs.existsSync(assetFolder)) return;
+
+ return fs.copyDir(
+ assetFolder,
+ that.resolve('gitbook'),
+ {
+ deleteFirst: false, // Delete "to" before
+ overwrite: true,
+ confirm: true
+ }
+ );
+ });
});
};
@@ -128,4 +163,4 @@ WebsiteOutput.prototype.templateName = function(name) {
return path.join(this.name, name+'.html');
};
-module.exports = conrefsLoader(WebsiteOutput);
+module.exports = WebsiteOutput;
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index c2f2bd344..42fd3c612 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -5,6 +5,8 @@ var rmdir = require('rmdir');
var tmp = require('tmp');
var request = require('request');
var path = require('path');
+var cp = require('cp');
+var cpr = require('cpr');
var Promise = require('./promise');
@@ -37,14 +39,6 @@ function writeStream(filename, st) {
return d.promise;
}
-// Copy a file using stream
-function copyFile(from, to) {
- return Promise()
- .then(function() {
- return writeStream(to, fs.createReadStream(from));
- });
-}
-
// Return a promise resolved with a boolean
function fileExists(filename) {
var d = Promise.defer();
@@ -113,7 +107,8 @@ module.exports = {
statSync: fs.statSync,
readdir: Promise.nfbind(fs.readdir),
writeStream: writeStream,
- copy: copyFile,
+ copy: Promise.nfbind(cp),
+ copyDir: Promise.nfbind(cpr),
tmpFile: genTmpFile,
tmpDir: genTmpDir,
download: download,
diff --git a/package.json b/package.json
index 03bc3d162..2ece788bd 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,6 @@
"resolve": "0.6.3",
"mkdirp": "0.5.1",
"error": "7.0.2",
- "fstream-ignore": "1.0.2",
"gitbook-parsers": "0.8.9",
"gitbook-plugin-highlight": "1.0.3",
"gitbook-plugin-sharing": "1.0.1",
@@ -45,7 +44,9 @@
"destroy": "1.0.4",
"ignore": "2.2.19",
"deprecated": "0.0.1",
- "rmdir": "1.2.0"
+ "rmdir": "1.2.0",
+ "cp": "0.2.0",
+ "cpr": "1.0.0"
},
"devDependencies": {
"eslint": "1.5.0",
From 52206ce9d0353bc01b7238fe86aa19a92ec6effb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 10:53:57 +0100
Subject: [PATCH 100/217] Add test for copy of assets in website generator
---
package.json | 1 +
test/mock.js | 6 +++++-
test/output-website.js | 23 +++++++++++++++++++++++
3 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 2ece788bd..5fceb40f9 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
"gitbook-plugin-fontsettings": "1.0.2",
+ "gitbook-plugin-theme-default": "git+https://github.com/GitbookIO/gitbook-theme-default.git#master",
"nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
diff --git a/test/mock.js b/test/mock.js
index 701df9999..fe8ed7ae3 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -5,6 +5,7 @@ var path = require('path');
var Book = require('../').Book;
var NodeFS = require('../lib/fs/node');
+var fs = require('../lib/utils/fs');
require('./assertions');
@@ -22,7 +23,10 @@ function setupFS(_fs, rootFolder, files) {
if (_.isObject(buf)) buf = JSON.stringify(buf);
if (_.isString(buf)) buf = new Buffer(buf, 'utf-8');
- return _fs.write(filename, buf);
+ return fs.mkdirp(path.dirname(filename))
+ .then(function() {
+ return fs.writeFile(filename, buf);
+ });
});
}, Q())
.value()
diff --git a/test/output-website.js b/test/output-website.js
index 86c9d2b2b..bc15bcc39 100644
--- a/test/output-website.js
+++ b/test/output-website.js
@@ -1,5 +1,28 @@
+var mock = require('./mock');
+var WebsiteOutput = require('../lib/output/website');
describe('Website Output', function() {
+ describe('Sample Book', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook(WebsiteOutput)
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ it('should correctly generate an index.html', function() {
+ output.should.have.file('index.html');
+ });
+
+ it('should correctly copy assets', function() {
+ output.should.have.file('gitbook/app.js');
+ output.should.have.file('gitbook/images/favicon.ico');
+ });
+
+ });
+
});
From 58a930e456860a025cb8565effe73fb3ae437882 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 11:50:33 +0100
Subject: [PATCH 101/217] Add "level" to TOCArticle
---
lib/backbone/summary.js | 34 +++++++++++++++++++---------------
lib/page/index.js | 1 +
2 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index a1caae7fe..405c192a6 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -11,29 +11,35 @@ 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(summary, title, ref, articles, parent) {
- this.summary = summary;
- this.title = title;
+function TOCArticle(def, parent) {
+ // Title
+ this.title = def.title;
+
+ // Parent TOCPart or TOCArticle
this.parent = parent;
- if (ref && location.isExternal(ref)) {
+ // As string indicating the overall position
+ // ex: '1.0.0'
+ this.level = def.level;
+
+ if (def.path && location.isExternal(def.path)) {
throw error.ParsingError(new Error('SUMMARY can only contains relative locations'));
}
- if (!title) {
+ if (!def.title) {
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
}
- var parts = url.parse(ref);
- this.ref = ref;
+ var parts = url.parse(def.path);
+ this.ref = def.path;
if (!this.isExternal()) {
this.path = parts.pathname;
this.anchor = parts.hash;
}
- this.articles = _.map(articles || [], function(article) {
+ this.articles = _.map(def.articles || [], function(article) {
if (article instanceof TOCArticle) return article;
- return new TOCArticle(article.title, article.ref, article.articles, this);
+ return new TOCArticle(article, this);
}, this);
}
@@ -48,6 +54,7 @@ TOCArticle.prototype.walk = function(iter) {
// Return templating context for an article
TOCArticle.prototype.getContext = function() {
return {
+ level: this.level,
title: this.title,
path: this.isExternal()? undefined : this.path,
anchor: this.isExternal()? undefined : this.anchor,
@@ -118,12 +125,9 @@ TOCArticle.prototype.map = function(iter) {
/*
A part of a ToC is a composed of a tree of articles.
*/
-function TOCPart(summary, part) {
- var that = this;
-
- this.summary = summary;
+function TOCPart(part) {
this.articles = _.map(part.articles || part.chapters, function(article) {
- return new TOCArticle(that.summary, article.title, article.path, article.articles, this);
+ return new TOCArticle(article, this);
}, this);
}
@@ -164,7 +168,7 @@ Summary.prototype.parse = function(content) {
// TODO: update GitBook's parsers to return a list of parts
.then(function(part) {
- that.parts = [new TOCPart(that, part)];
+ that.parts = [new TOCPart(part)];
// Update count of articles
that._length = 0;
diff --git a/lib/page/index.js b/lib/page/index.js
index c92bdfa20..51ed2aa53 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -102,6 +102,7 @@ Page.prototype.getContext = function() {
title: article? article.title : null,
next: next? next.getContext() : null,
previous: prev? prev.getContext() : null,
+ level: article? article.level : null,
content: this.content
}
};
From 576d773fecf5ec6a7ebf5b089ff7a03b102468eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 12:13:24 +0100
Subject: [PATCH 102/217] Correctly map filenames for website output
---
lib/output/base.js | 3 ++-
test/assertions.js | 2 +-
test/mock.js | 2 +-
test/output-website.js | 33 +++++++++++++++++++++++++++++++++
4 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index e674f4652..09f57bc38 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -184,9 +184,10 @@ Output.prototype.resolveForPage = function(page, href) {
// Filename for output
// READMEs are replaced by index.html
+// /test/README.md -> /test/index.html
Output.prototype.outputPath = function(filename, ext) {
ext = ext || this.defaultExtension;
- var output;
+ var output = filename;
if (
path.basename(filename, path.extname(filename)) == 'README' ||
diff --git a/test/assertions.js b/test/assertions.js
index b9369953d..56da24917 100644
--- a/test/assertions.js
+++ b/test/assertions.js
@@ -6,7 +6,7 @@ var should = require('should');
// Assertions to test if an Output has generated a file
should.Assertion.add('file', function(file, description) {
this.params = {
- actual: this.obj.toString(),
+ actual: this.obj.root(),
operator: 'have file ' + file,
message: description
};
diff --git a/test/mock.js b/test/mock.js
index fe8ed7ae3..f9cc65789 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -68,7 +68,7 @@ function setupDefaultBook(files, summary, opts) {
// Output a book with a specific generator
function outputDefaultBook(Output, files, summary, opts) {
- return setupDefaultBook(files, opts)
+ return setupDefaultBook(files, summary, opts)
.then(function(book) {
// Parse the book
return book.parse()
diff --git a/test/output-website.js b/test/output-website.js
index bc15bcc39..9fe1928a1 100644
--- a/test/output-website.js
+++ b/test/output-website.js
@@ -24,5 +24,38 @@ describe('Website Output', function() {
});
+ describe('Book with chapters', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook(WebsiteOutput, {
+ 'hello/README.md': '# Hello',
+ 'hello/test.md': '# Test'
+ }, [
+ {
+ title: 'Hello',
+ path: 'hello/README.md'
+ },
+ {
+ title: 'Test',
+ path: 'hello/test.md'
+ }
+ ])
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ it('should correctly generate an index.html', function() {
+ output.should.have.file('index.html');
+ });
+
+ it('should correctly generate files in folder', function() {
+ output.should.have.file('hello/index.html');
+ output.should.have.file('hello/test.html');
+ });
+
+ });
+
});
From 202c235c665ff2dc007a013b45bff787ee82176b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 12:15:53 +0100
Subject: [PATCH 103/217] Add tests for path utils module
---
test/all.js | 1 +
test/paths.js | 16 ++++++++++++++++
2 files changed, 17 insertions(+)
create mode 100644 test/paths.js
diff --git a/test/all.js b/test/all.js
index 97913d7f3..d8f227561 100644
--- a/test/all.js
+++ b/test/all.js
@@ -1,6 +1,7 @@
// Utilities
require('./location');
+require('./paths');
// Parsing
require('./config');
diff --git a/test/paths.js b/test/paths.js
new file mode 100644
index 000000000..c7929d346
--- /dev/null
+++ b/test/paths.js
@@ -0,0 +1,16 @@
+var pathUtils = require('../lib/utils/path');
+
+describe('Paths', function() {
+
+ describe('setExtension', function() {
+ it('should correctly change extension of filename', function() {
+ pathUtils.setExtension('test.md', '.html').should.be.equal('test.html');
+ pathUtils.setExtension('test.md', '.json').should.be.equal('test.json');
+ });
+
+ it('should correctly change extension of path', function() {
+ pathUtils.setExtension('hello/test.md', '.html').should.be.equal('hello/test.html');
+ pathUtils.setExtension('hello/test.md', '.json').should.be.equal('hello/test.json');
+ });
+ });
+});
From 6e83240233e6168aa6567eb6fcac62508fe7fd0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 13:58:10 +0100
Subject: [PATCH 104/217] Glossary should be output as a normal page
---
lib/backbone/glossary.js | 21 +++++++++++++++++++++
lib/output/base.js | 1 +
lib/page/html.js | 2 +-
lib/page/index.js | 9 +--------
4 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js
index 9aed1ac35..45848e0ae 100644
--- a/lib/backbone/glossary.js
+++ b/lib/backbone/glossary.js
@@ -44,6 +44,15 @@ util.inherits(Glossary, BackboneFile);
Glossary.prototype.type = 'glossary';
+// Get templating context
+Glossary.prototype.getContext = function() {
+ return {
+ glossary: {
+ path: this.path
+ }
+ };
+};
+
// Parse the readme content
Glossary.prototype.parse = function(content) {
var that = this;
@@ -73,4 +82,16 @@ 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/output/base.js b/lib/output/base.js
index 09f57bc38..d22fda1eb 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -169,6 +169,7 @@ Output.prototype.getPageContext = function(page) {
gitbook.getContext(),
this.book.getContext(),
this.book.summary.getContext(),
+ this.book.glossary.getContext(),
this.book.config.getContext()
);
};
diff --git a/lib/page/html.js b/lib/page/html.js
index d667059d9..45fae0d7c 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -164,7 +164,7 @@ HTMLPipeline.prototype.applyAnnotations = function() {
replaceText(that.$, this, searchRegex, function(match) {
that.opts.onAnnotation(annotation);
- return ''
+ match
+ ' ';
diff --git a/lib/page/index.js b/lib/page/index.js
index 51ed2aa53..bdf3c81b4 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -166,14 +166,7 @@ Page.prototype.toHTML = function(output) {
},
// Convert glossary entries to annotations
- annotations: _.map(that.book.glossary.entries, function(entry) {
- return {
- id: entry.id,
- name: entry.name,
- description: entry.description,
- href: '/GLOSSARY.html'
- };
- })
+ annotations: that.book.glosary.annotations()
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
From 709b388dfcc641fab25d297618b6ffe49f5cd677 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 14:47:53 +0100
Subject: [PATCH 105/217] Fix path calcul to be coherant
---
lib/output/assets-inliner.js | 2 +-
lib/output/base.js | 12 +++-
lib/output/website.js | 2 +-
lib/page/html.js | 13 ++++-
lib/page/index.js | 22 +++++---
lib/utils/location.js | 3 +-
test/location.js | 16 +++++-
test/page.js | 104 +++++++++++++++++++++++++++--------
8 files changed, 135 insertions(+), 39 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index 8d7c48e79..6768830e0 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -73,7 +73,7 @@ module.exports = function assetsInliner(Base) {
// Return relative path from the page
.then(function(filename) {
- return page.relative('/' + filename);
+ return page.relative(filename);
});
};
diff --git a/lib/output/base.js b/lib/output/base.js
index d22fda1eb..6678bb092 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -130,10 +130,18 @@ Output.prototype.finish = function() {
// Resolve an HTML link
Output.prototype.onRelativeLink = function(currentPage, href) {
- var to = this.book.getPage(href);
+ var to = currentPage.followPage(href);
// Replace by an .html link
- if (to) href = this.outputUrl(to.path);
+ if (to) {
+ href = to.path;
+
+ // Recalcul as relative link
+ href = currentPage.relative(href);
+
+ // Replace .md by .html
+ href = this.outputUrl(href);
+ }
return href;
};
diff --git a/lib/output/website.js b/lib/output/website.js
index b59589772..d85622972 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -87,7 +87,7 @@ WebsiteOutput.prototype.prepare = function() {
// Transform a '.md' into a '.html' (README -> index)
that.env.addFilter('contentURL', function(s) {
- return that.onRelativeLink(null, s);
+ return that.outputUrl(s);
});
// Relase path to an asset
diff --git a/lib/page/html.js b/lib/page/html.js
index 45fae0d7c..bd9ec910d 100644
--- a/lib/page/html.js
+++ b/lib/page/html.js
@@ -1,4 +1,5 @@
var _ = require('lodash');
+var url = require('url');
var cheerio = require('cheerio');
var domSerializer = require('dom-serializer');
var slug = require('github-slugid');
@@ -63,7 +64,11 @@ HTMLPipeline.prototype.transformLinks = function() {
if (location.isAnchor(href)) {
// Don't "change" anchor links
} else if (location.isRelative(href)) {
- $a.attr('href', this.opts.onRelativeLink(href));
+ // Preserve anchor
+ var parsed = url.parse(href);
+ var filename = this.opts.onRelativeLink(parsed.pathname);
+
+ $a.attr('href', filename + (parsed.hash || ''));
} else {
// External links
$a.attr('target', '_blank');
@@ -178,12 +183,16 @@ HTMLPipeline.prototype.output = function() {
var that = this;
return Promise()
- .then(this.transformLinks)
.then(this.transformImages)
.then(this.transformHeadings)
.then(this.transformCodeBlocks)
.then(this.transformSvgs)
.then(this.applyAnnotations)
+
+ // Transform of links should be applied after annotations
+ // because annotations are created as links
+ .then(this.transformLinks)
+
.then(function() {
return renderDOM(that.$);
});
diff --git a/lib/page/index.js b/lib/page/index.js
index bdf3c81b4..bc12529d3 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -60,14 +60,23 @@ Page.prototype.resolve = function() {
return this.book.resolve(this.resolveLocal.apply(this, arguments));
};
-// Convert an absolite path to a relative path from this page
+// Convert an absolute path (in the book) to a relative path from this page
Page.prototype.relative = function(name) {
+ // Convert /test.png -> test.png
+ name = location.toAbsolute(name, '', '');
+
return location.relative(
- this.resolve('.'),
- this.resolve(name)
+ this.resolve('.') + '/',
+ this.book.resolve(name)
);
};
+// Return a page result of a relative page from this page
+Page.prototype.followPage = function(filename) {
+ var absPath = this.resolveLocal(filename);
+ return this.book.getPage(absPath);
+};
+
// Update content of the page
Page.prototype.update = function(content) {
this.content = content;
@@ -148,10 +157,7 @@ Page.prototype.toHTML = function(output) {
// Normalize HTML output
.then(function() {
var pipelineOpts = {
- onRelativeLink: function (href) {
- href = that.relative(href);
- return output.onRelativeLink(that, href);
- },
+ onRelativeLink: _.partial(output.onRelativeLink, that),
onImage: _.partial(output.onOutputImage, that),
onOutputSVG: _.partial(output.onOutputSVG, that),
@@ -166,7 +172,7 @@ Page.prototype.toHTML = function(output) {
},
// Convert glossary entries to annotations
- annotations: that.book.glosary.annotations()
+ annotations: that.book.glossary.annotations()
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
diff --git a/lib/utils/location.js b/lib/utils/location.js
index 09fa93a60..d96cf85ee 100644
--- a/lib/utils/location.js
+++ b/lib/utils/location.js
@@ -51,7 +51,8 @@ function toAbsolute(_href, dir, outdir) {
return _href;
}
-// Convert an absolute path to a relative patg
+// Convert an absolute path to a relative path for a specific folder (dir)
+// ('test/', 'hello.md') -> '../hello.md'
function relative(dir, file) {
return normalize(path.relative(dir, file));
}
diff --git a/test/location.js b/test/location.js
index 4d949facd..2f209e08c 100644
--- a/test/location.js
+++ b/test/location.js
@@ -16,7 +16,21 @@ describe('Location', function() {
location.isAnchor('test.md#test').should.be.exactly(false);
});
- describe('toAbsolute', function() {
+ describe('.relative', function() {
+ it('should resolve to a relative path (same folder)', function() {
+ location.relative('links/', 'links/test.md').should.equal('test.md');
+ });
+
+ it('should resolve to a relative path (parent folder)', function() {
+ location.relative('links/', 'test.md').should.equal('../test.md');
+ });
+
+ it('should resolve to a relative path (child folder)', function() {
+ location.relative('links/', 'links/hello/test.md').should.equal('hello/test.md');
+ });
+ });
+
+ describe('.toAbsolute', function() {
it('should correctly transform as absolute', function() {
location.toAbsolute('http://google.fr').should.be.equal('http://google.fr');
location.toAbsolute('test.md', './', './').should.be.equal('test.md');
diff --git a/test/page.js b/test/page.js
index b68e894e9..41a08934b 100644
--- a/test/page.js
+++ b/test/page.js
@@ -7,7 +7,9 @@ describe('Page', function() {
before(function() {
return mock.setupDefaultBook({
'heading.md': '# Hello\n\n## World',
+
'links.md': '[link](hello.md) [link 2](variables/page/next.md) [readme](README.md)',
+ 'links/relative.md': '[link](../hello.md) [link 2](/variables/page/next.md) [readme](../README.md)',
'annotations/simple.md': 'A magicien say abracadabra!',
'annotations/code.md': 'A magicien say `abracadabra`!',
@@ -71,12 +73,12 @@ describe('Page', function() {
describe('.relative', function() {
it('should correctly resolve absolute path in the book', function() {
var page = book.addPage('heading.md');
- var page2 = book.addPage('folder/paths.md');
-
page.relative('/test.png').should.equal('test.png');
page.relative('test.png').should.equal('test.png');
+
+ var page2 = book.addPage('folder/paths.md');
page2.relative('/test.png').should.equal('../test.png');
- page2.relative('test.png').should.equal('test.png');
+ page2.relative('test.png').should.equal('../test.png');
});
});
@@ -126,35 +128,91 @@ describe('Page', function() {
});
});
- describe('Links', function() {
+ describe('.resolve', function() {
var page;
before(function() {
- page = book.addPage('links.md');
- return page.toHTML(output);
+ page = book.addPage('links/relative.md');
});
- it('should replace links to page to .html', function() {
- page.content.should.be.html({
- 'a[href="./"]': {
- count: 1
- }
+ it('should resolve to a relative path (same folder)', function() {
+ page.relative('links/test.md').should.equal('test.md');
+ });
+
+ it('should resolve to a relative path (parent folder)', function() {
+ page.relative('test.md').should.equal('../test.md');
+ page.relative('hello/test.md').should.equal('../hello/test.md');
+ });
+
+ it('should resolve to a relative path (child folder)', function() {
+ page.relative('links/hello/test.md').should.equal('hello/test.md');
+ });
+ });
+
+ describe('Links', function() {
+ describe('From base directory', function() {
+ var page;
+
+ before(function() {
+ page = book.addPage('links.md');
+ return page.toHTML(output);
+ });
+
+ it('should replace links to page to .html', function() {
+ page.content.should.be.html({
+ 'a[href="variables/page/next.html"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should use directory urls when file is a README', function() {
+ page.content.should.be.html({
+ 'a[href="./"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should not replace links to file not in SUMMARY', function() {
+ page.content.should.be.html({
+ 'a[href="hello.md"]': {
+ count: 1
+ }
+ });
});
});
- it('should use directory urls when file is a README', function() {
- page.content.should.be.html({
- 'a[href="./"]': {
- count: 1
- }
- });
- });
+ describe('From sub-directory', function() {
+ var page;
- it('should not replace links to file not in SUMMARY', function() {
- page.content.should.be.html({
- 'a[href="hello.md"]': {
- count: 1
- }
+ before(function() {
+ page = book.addPage('links/relative.md');
+ return page.toHTML(output);
+ });
+
+ it('should replace links to page to .html', function() {
+ page.content.should.be.html({
+ 'a[href="../variables/page/next.html"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should use directory urls when file is a README', function() {
+ page.content.should.be.html({
+ 'a[href="../"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should not replace links to file not in SUMMARY', function() {
+ page.content.should.be.html({
+ 'a[href="../hello.md"]': {
+ count: 1
+ }
+ });
});
});
});
From b99c499d2ea67268760a7c8265a0669b016a56f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 15:08:34 +0100
Subject: [PATCH 106/217] Add back serve command
---
lib/cli/helper.js | 4 +-
lib/cli/index.js | 111 +++++++++++++++++++++++++++++++++++++++++++++-
lib/cli/server.js | 95 +++++++++++++++++++++++++++++++++++++++
lib/cli/watch.js | 42 ++++++++++++++++++
4 files changed, 249 insertions(+), 3 deletions(-)
create mode 100644 lib/cli/server.js
create mode 100644 lib/cli/watch.js
diff --git a/lib/cli/helper.js b/lib/cli/helper.js
index e8a049fc8..1086b4da6 100644
--- a/lib/cli/helper.js
+++ b/lib/cli/helper.js
@@ -63,5 +63,7 @@ module.exports = {
options: {
log: LOG_OPTION,
format: FORMAT_OPTION
- }
+ },
+
+ FORMATS: FORMATS
};
diff --git a/lib/cli/index.js b/lib/cli/index.js
index c9a2e8f65..0492c298f 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -1,7 +1,16 @@
-var path = require('path');
+/* eslint-disable no-console */
+var path = require('path');
+var tinylr = require('tiny-lr');
+
+var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
+var Book = require('../book');
+var NodeFS = require('../fs/node');
+
var helper = require('./helper');
+var Server = require('./server');
+var watch = require('./watch');
module.exports = {
commands: [
@@ -60,8 +69,106 @@ module.exports = {
return output.generate();
});
})
- }
+ },
+ {
+ name: 'serve [book]',
+ description: 'Build then serve a book from a directory',
+ options: [
+ {
+ name: 'port',
+ description: 'Port for server to listen on',
+ defaults: 4000
+ },
+ {
+ name: 'lrport',
+ description: 'Port for livereload server to listen on',
+ defaults: 35729
+ },
+ {
+ name: 'watch',
+ description: 'Enable/disable file watcher',
+ defaults: true
+ },
+ helper.options.format,
+ helper.options.log
+ ],
+ exec: function(args, kwargs) {
+ var input = args[0] || process.cwd();
+ var server = new Server();
+
+ // Init livereload server
+ var lrServer = tinylr({});
+ var port = kwargs.port;
+ var lrPath;
+
+ var generate = function() {
+
+ // Stop server if running
+ if (server.isRunning()) console.log('Stopping server');
+ return server.stop()
+
+ // Generate the book
+ .then(function() {
+ var book = new Book({
+ fs: new NodeFS(),
+ root: input,
+ 'config': {
+ 'defaultsPlugins': ['livereload']
+ },
+ 'logLevel': kwargs.log
+ });
+
+ return book.parse()
+ .then(function() {
+ var Out = helper.FORMATS[kwargs.format];
+ var output = new Out(book);
+
+ return output.generate()
+ .thenResolve(output);
+ });
+ })
+
+ // Start server and watch changes
+ .then(function(output) {
+ console.log();
+ console.log('Starting server ...');
+ return server.start(output.root(), port)
+ .then(function() {
+ console.log('Serving book on http://localhost:'+port);
+
+ if (lrPath) {
+ // trigger livereload
+ lrServer.changed({
+ body: {
+ files: [lrPath]
+ }
+ });
+ }
+
+ if (!kwargs.watch) return;
+
+ return watch(output.book.root)
+ .then(function(filepath) {
+ // set livereload path
+ lrPath = filepath;
+ console.log('Restart after change in file', filepath);
+ console.log('');
+ return generate();
+ });
+ });
+ });
+ };
+
+ return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
+ .then(function() {
+ console.log('Live reload server started on port:', kwargs.lrport);
+ console.log('Press CTRL+C to quit ...');
+ console.log('');
+ return generate();
+ });
+ }
+ }
]
};
diff --git a/lib/cli/server.js b/lib/cli/server.js
new file mode 100644
index 000000000..3bd5d1813
--- /dev/null
+++ b/lib/cli/server.js
@@ -0,0 +1,95 @@
+var events = require('events');
+var http = require('http');
+var send = require('send');
+var util = require('util');
+var url = require('url');
+
+var Promise = require('../utils/promise');
+
+var Server = function() {
+ this.running = null;
+ this.dir = null;
+ this.port = 0;
+ this.sockets = [];
+};
+util.inherits(Server, events.EventEmitter);
+
+// Return true if the server is running
+Server.prototype.isRunning = function() {
+ return !!this.running;
+};
+
+// Stop the server
+Server.prototype.stop = function() {
+ var that = this;
+ if (!this.isRunning()) return Promise();
+
+ var d = Promise.defer();
+ this.running.close(function(err) {
+ that.running = null;
+ that.emit('state', false);
+
+ if (err) d.reject(err);
+ else d.resolve();
+ });
+
+ for (var i = 0; i < this.sockets.length; i++) {
+ this.sockets[i].destroy();
+ }
+
+ return d.promise;
+};
+
+Server.prototype.start = function(dir, port) {
+ var that = this, pre = Promise();
+ port = port || 8004;
+
+ if (that.isRunning()) pre = this.stop();
+ return pre
+ .then(function() {
+ var d = Promise.defer();
+
+ that.running = http.createServer(function(req, res){
+ // Render error
+ function error(err) {
+ res.statusCode = err.status || 500;
+ res.end(err.message);
+ }
+
+ // Redirect to directory's index.html
+ function redirect() {
+ res.statusCode = 301;
+ res.setHeader('Location', req.url + '/');
+ res.end('Redirecting to ' + req.url + '/');
+ }
+
+ // Send file
+ send(req, url.parse(req.url).pathname)
+ .root(dir)
+ .on('error', error)
+ .on('directory', redirect)
+ .pipe(res);
+ });
+
+ that.running.on('connection', function (socket) {
+ that.sockets.push(socket);
+ socket.setTimeout(4000);
+ socket.on('close', function () {
+ that.sockets.splice(that.sockets.indexOf(socket), 1);
+ });
+ });
+
+ that.running.listen(port, function(err) {
+ if (err) return d.reject(err);
+
+ that.port = port;
+ that.dir = dir;
+ that.emit('state', true);
+ d.resolve();
+ });
+
+ return d.promise;
+ });
+};
+
+module.exports = Server;
diff --git a/lib/cli/watch.js b/lib/cli/watch.js
new file mode 100644
index 000000000..36c42ccd1
--- /dev/null
+++ b/lib/cli/watch.js
@@ -0,0 +1,42 @@
+var _ = require('lodash');
+var path = require('path');
+var chokidar = require('chokidar');
+var parsers = require('gitbook-parsers');
+
+var Promise = require('../utils/promise');
+
+// Watch a folder and resolve promise once a file is modified
+function watch(dir) {
+ var d = Promise.defer();
+ dir = path.resolve(dir);
+
+ var toWatch = [
+ 'book.json', 'book.js'
+ ];
+
+ // Watch all parsable files
+ _.each(parsers.extensions, function(ext) {
+ toWatch.push('**/*'+ext);
+ });
+
+ var watcher = chokidar.watch(toWatch, {
+ cwd: dir,
+ ignored: '_book/**',
+ ignoreInitial: true
+ });
+
+ watcher.once('all', function(e, filepath) {
+ watcher.close();
+
+ d.resolve(filepath);
+ });
+ watcher.once('error', function(err) {
+ watcher.close();
+
+ d.reject(err);
+ });
+
+ return d.promise;
+}
+
+module.exports = watch;
From 1a3ee784592c8cfbf1a6bc98dcaf3e8a5afef7fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 15:23:01 +0100
Subject: [PATCH 107/217] Accept external url in summary
---
lib/backbone/summary.js | 3 ---
1 file changed, 3 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 405c192a6..0d1d0d7a9 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -22,9 +22,6 @@ function TOCArticle(def, parent) {
// ex: '1.0.0'
this.level = def.level;
- if (def.path && location.isExternal(def.path)) {
- throw error.ParsingError(new Error('SUMMARY can only contains relative locations'));
- }
if (!def.title) {
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
}
From 3fcf1a7723a5a3b7a436974c445dbb2b79fad583 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 15:23:15 +0100
Subject: [PATCH 108/217] Dont fail if summary doesn't exist
---
lib/book.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/book.js b/lib/book.js
index acfaecea3..40c96861c 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -205,12 +205,12 @@ Book.prototype.parse = function() {
.then(that.summary.load)
.then(function() {
if (!that.summary.exists()) {
- throw new error.FileNotFoundError({ filename: 'SUMMARY' });
+ that.log.warn.ln('no summary file in this book');
}
// Index summary's articles
that.summary.walk(function(article) {
- if (!article.hasLocation()) return;
+ if (!article.hasLocation() || article.isExternal()) return;
that.addPage(article.path);
});
})
From 8401a674c382cd1c7ca6c6a02e4def3d237c9734 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 16:34:11 +0100
Subject: [PATCH 109/217] Add test for navigation in summary
---
lib/backbone/summary.js | 79 +++++++++++++++-------
test/git.js | 6 +-
test/parse.js | 4 +-
test/summary.js | 143 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 203 insertions(+), 29 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 0d1d0d7a9..8e1a66d8b 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -79,38 +79,62 @@ TOCArticle.prototype.hasParent = function() {
return (this.parent instanceof TOCArticle);
};
-// Return next article in the TOC
-TOCArticle.prototype.next = function() {
+// Return a sibling (next or prev) in the parent
+// Withotu taking in consideration children/parent
+TOCArticle.prototype.sibling = function(direction) {
var parentsArticles = this.parent.articles;
var pos = _.findIndex(parentsArticles, this);
- if ((pos + 1) >= parentsArticles.length) {
- if (this.hasParent()) {
- return this.parent.next();
- } else {
- return null;
- }
- } else {
- // next has the same parent
- return parentsArticles[pos + 1];
+ if (parentsArticles[pos + direction]) {
+ return parentsArticles[pos + direction];
}
+
+ return null;
+};
+
+// Return a sibling (next or prev)
+// It takes parents.children in consideration
+TOCArticle.prototype._sibling = function(direction) {
+ // Next should go to the first children
+ if (direction > 0 && this.hasChildren()) {
+ return _.first(this.articles);
+ }
+
+ var parentsArticles = this.parent.articles;
+ var pos = _.findIndex(parentsArticles, this);
+
+ // First child and has parent
+ if (pos == 0 && direction < 0 && this.hasParent()) {
+ return this.parent;
+ }
+
+ // Last child and has parent
+ if(pos == (parentsArticles.length - 1) && direction > 0 && this.hasParent()) {
+ return this.parent.sibling(1);
+ }
+
+ if (parentsArticles[pos + direction]) {
+ var article = parentsArticles[pos + direction];
+
+ // If goign back, take last children from "brother"
+ if (direction < 0 && article.hasChildren()) {
+ article = _.last(article.articles);
+ }
+
+ return article;
+ }
+
+ return null;
+};
+
+// Return next article in the TOC
+TOCArticle.prototype.next = function() {
+ return this._sibling(1);
};
// Return previous article in the TOC
TOCArticle.prototype.prev = function() {
- var parentsArticles = this.parent.articles;
- var pos = _.findIndex(parentsArticles, this);
-
- if ((pos - 1) < 0) {
- if (this.hasParent()) {
- return this.parent.prev();
- } else {
- return null;
- }
- } else {
- // prev has the same parent
- return parentsArticles[pos - 1];
- }
+ return this._sibling(-1);
};
// Map over all articles
@@ -228,6 +252,13 @@ Summary.prototype.getArticle = function(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;
diff --git a/test/git.js b/test/git.js
index 8667b0722..9fd749033 100644
--- a/test/git.js
+++ b/test/git.js
@@ -10,14 +10,14 @@ describe('Git', function() {
it('should correctly validate git urls', function() {
// HTTPS
- Git.isUrl('git+https://github.com/Hello/world.git').should.be.ok;
+ Git.isUrl('git+https://github.com/Hello/world.git').should.be.ok();
// SSH
Git.isUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1').should.be.ok;
// Non valid
- Git.isUrl('https://github.com/Hello/world.git').should.not.be.ok;
- Git.isUrl('README.md').should.not.be.ok;
+ Git.isUrl('https://github.com/Hello/world.git').should.not.be.ok();
+ Git.isUrl('README.md').should.not.be.ok();
});
it('should parse HTTPS urls', function() {
diff --git a/test/parse.js b/test/parse.js
index e1901072b..63565eed1 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -1,12 +1,12 @@
var mock = require('./mock');
describe('Parsing', function() {
- it('should fail without SUMMARY', function() {
+ it('should not fail without SUMMARY', function() {
return mock.setupBook({
'README.md': ''
})
.then(function(book) {
- return book.parse().should.be.rejected;
+ return book.parse().should.be.fulfilled();
});
});
diff --git a/test/summary.js b/test/summary.js
index 2744c430a..a9aea5dcf 100644
--- a/test/summary.js
+++ b/test/summary.js
@@ -1,3 +1,5 @@
+var should = require('should');
+
var mock = require('./mock');
describe('Summary / Table of contents', function() {
@@ -36,5 +38,146 @@ describe('Summary / Table of contents', function() {
book.summary.count().should.equal(3);
});
});
+
+ describe('External', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({}, [
+ {
+ title: 'Google',
+ path: 'https://www.google.fr'
+ }
+ ])
+ .then(function(_book) {
+ book = _book;
+ return book.summary.load();
+ });
+ });
+
+ it('should correctly count articles', function() {
+ book.summary.count().should.equal(2);
+ });
+
+ it('should correctly signal it as external', function() {
+ var article = book.summary.getArticleByLevel('1');
+
+ should(article).be.ok();
+ should(article.path).not.be.ok();
+
+ article.title.should.equal('Google');
+ article.ref.should.equal('https:/www.google.fr');
+ article.isExternal().should.be.ok;
+ });
+ });
+
+ describe('Next / Previous', function() {
+ var book;
+
+ before(function() {
+ return mock.setupDefaultBook({
+ 'SUMMARY.md': '# Summary\n\n' +
+ '* [Hello](hello.md)\n' +
+ '* [Hello 2](hello2.md)\n' +
+ ' * [Hello 3](hello3.md)\n' +
+ ' * [Hello 4](hello4.md)\n' +
+ ' * [Hello 5](hello5.md)\n' +
+ '* [Hello 6](hello6.md)\n'
+ })
+ .then(function(_book) {
+ book = _book;
+ return book.summary.load();
+ });
+ });
+
+ it('should only return a next for the readme', function() {
+ var article = book.summary.getArticle('README.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).equal(null);
+ should(next).be.ok();
+
+ next.path.should.equal('hello.md');
+ });
+
+ it('should return next/prev for a first level page', function() {
+ var article = book.summary.getArticle('hello.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('README.md');
+ next.path.should.equal('hello2.md');
+ });
+
+ it('should return next/prev for a joint -> child', function() {
+ var article = book.summary.getArticle('hello2.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello.md');
+ next.path.should.equal('hello3.md');
+ });
+
+ it('should return next/prev for a joint <- child', function() {
+ var article = book.summary.getArticle('hello3.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello2.md');
+ next.path.should.equal('hello4.md');
+ });
+
+ it('should return next/prev for a children', function() {
+ var article = book.summary.getArticle('hello4.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello3.md');
+ next.path.should.equal('hello5.md');
+ });
+
+ it('should return next/prev for a joint -> parent', function() {
+ var article = book.summary.getArticle('hello5.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello4.md');
+ next.path.should.equal('hello6.md');
+ });
+
+ it('should return prev for last', function() {
+ var article = book.summary.getArticle('hello6.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.not.ok();
+
+ prev.path.should.equal('hello5.md');
+ });
+ });
});
From 95d81d02a12dc6ae5e9d845e2eb69b7e1dc1debd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 17:03:37 +0100
Subject: [PATCH 110/217] Fix resolve of images
---
lib/cli/helper.js | 3 +-
lib/cli/index.js | 2 +-
lib/output/base.js | 5 ++-
lib/output/folder.js | 7 ++-
test/page.js | 105 ++++++++++++++++++++++++++++++++++---------
5 files changed, 96 insertions(+), 26 deletions(-)
diff --git a/lib/cli/helper.js b/lib/cli/helper.js
index 1086b4da6..6dae58c24 100644
--- a/lib/cli/helper.js
+++ b/lib/cli/helper.js
@@ -1,4 +1,5 @@
var _ = require('lodash');
+var path = require('path');
var Book = require('../book');
var NodeFS = require('../fs/node');
@@ -36,7 +37,7 @@ var FORMATS = {
// the root of the book is the first argument (or current directory)
function bookCmd(fn) {
return function(args, kwargs) {
- var input = args[0] || process.cwd();
+ var input = path.resolve(args[0] || process.cwd());
var book = new Book({
fs: new NodeFS(),
root: input,
diff --git a/lib/cli/index.js b/lib/cli/index.js
index 0492c298f..07c4888b0 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -94,7 +94,7 @@ module.exports = {
helper.options.log
],
exec: function(args, kwargs) {
- var input = args[0] || process.cwd();
+ var input = path.resolve(args[0] || process.cwd());
var server = new Server();
// Init livereload server
diff --git a/lib/output/base.js b/lib/output/base.js
index 6678bb092..2a7ec488e 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -76,12 +76,12 @@ Output.prototype.generate = function() {
.value();
return Promise.serie(byTypes.asset, function(filename) {
- that.log.info.ln('copy asset', filename);
+ that.log.debug.ln('copy asset', filename);
return that.onAsset(filename);
})
.then(function() {
return Promise.serie(byTypes.page, function(filename) {
- that.log.info.ln('process page', filename);
+ that.log.debug.ln('process page', filename);
return that.onPage(that.book.getPage(filename));
});
});
@@ -154,6 +154,7 @@ Output.prototype.onOutputSVG = function(page, svg) {
// Output an image as a file
// Normalize the relative link
Output.prototype.onOutputImage = function(page, imgFile) {
+ imgFile = page.resolveLocal(imgFile);
return page.relative(imgFile);
};
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 14e3f227f..9e139c0d9 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -88,7 +88,12 @@ module.exports = function folderOutput(Base) {
return Promise()
.then(function() {
to = that.resolve(to);
+ var folder = path.dirname(to);
+ // Ensure folder exists
+ return fs.mkdirp(folder);
+ })
+ .then(function() {
return fs.copy(from, to);
});
};
@@ -102,7 +107,7 @@ module.exports = function folderOutput(Base) {
filename = that.resolve(filename);
var folder = path.dirname(filename);
- // Ensure fodler exists
+ // Ensure folder exists
return fs.mkdirp(folder);
})
diff --git a/test/page.js b/test/page.js
index 41a08934b..52d86d8e5 100644
--- a/test/page.js
+++ b/test/page.js
@@ -11,6 +11,9 @@ describe('Page', function() {
'links.md': '[link](hello.md) [link 2](variables/page/next.md) [readme](README.md)',
'links/relative.md': '[link](../hello.md) [link 2](/variables/page/next.md) [readme](../README.md)',
+ 'images.md': '  ',
+ 'images/relative.md': ' ',
+
'annotations/simple.md': 'A magicien say abracadabra!',
'annotations/code.md': 'A magicien say `abracadabra`!',
'annotations/class.md': 'A magicien say abracadabra , right?
!',
@@ -82,6 +85,27 @@ describe('Page', function() {
});
});
+ describe('.resolve', function() {
+ var page;
+
+ before(function() {
+ page = book.addPage('links/relative.md');
+ });
+
+ it('should resolve to a relative path (same folder)', function() {
+ page.relative('links/test.md').should.equal('test.md');
+ });
+
+ it('should resolve to a relative path (parent folder)', function() {
+ page.relative('test.md').should.equal('../test.md');
+ page.relative('hello/test.md').should.equal('../hello/test.md');
+ });
+
+ it('should resolve to a relative path (child folder)', function() {
+ page.relative('links/hello/test.md').should.equal('hello/test.md');
+ });
+ });
+
describe('Headings', function() {
it('should add a default ID to headings', function() {
var page = book.addPage('heading.md');
@@ -128,27 +152,6 @@ describe('Page', function() {
});
});
- describe('.resolve', function() {
- var page;
-
- before(function() {
- page = book.addPage('links/relative.md');
- });
-
- it('should resolve to a relative path (same folder)', function() {
- page.relative('links/test.md').should.equal('test.md');
- });
-
- it('should resolve to a relative path (parent folder)', function() {
- page.relative('test.md').should.equal('../test.md');
- page.relative('hello/test.md').should.equal('../hello/test.md');
- });
-
- it('should resolve to a relative path (child folder)', function() {
- page.relative('links/hello/test.md').should.equal('hello/test.md');
- });
- });
-
describe('Links', function() {
describe('From base directory', function() {
var page;
@@ -217,6 +220,66 @@ describe('Page', function() {
});
});
+ describe('Images', function() {
+ describe('From base directory', function() {
+ var page;
+
+ before(function() {
+ page = book.addPage('images.md');
+ return page.toHTML(output);
+ });
+
+ it('should resolve relative images', function() {
+ page.content.should.be.html({
+ 'img[src="test.png"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should resolve absolute images', function() {
+ page.content.should.be.html({
+ 'img[src="test2.png"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should keep external images path', function() {
+ page.content.should.be.html({
+ 'img[src="https:/upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"]': {
+ count: 1
+ }
+ });
+ });
+ });
+
+ describe('From sub-directory', function() {
+ var page;
+
+ before(function() {
+ page = book.addPage('images/relative.md');
+ return page.toHTML(output);
+ });
+
+ it('should resolve relative images', function() {
+ page.content.should.be.html({
+ 'img[src="test.png"]': {
+ count: 1
+ }
+ });
+ });
+
+ it('should resolve absolute images', function() {
+ page.content.should.be.html({
+ 'img[src="../test2.png"]': {
+ count: 1
+ }
+ });
+ });
+ });
+ });
+
describe('Templating Context', function() {
it('should set file.mtime', function() {
var page = book.addPage('variables/file/mtime.md');
From 3d3302e547929a4f728aefa89ad7634f234fa726 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Thu, 18 Feb 2016 17:50:10 +0100
Subject: [PATCH 111/217] Add detection of text direction and provide:
'page.dir'
---
docs/variables.md | 1 +
lib/page/index.js | 11 ++++++++++-
package.json | 4 ++--
test/page.js | 16 ++++++++++++++++
4 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/docs/variables.md b/docs/variables.md
index fb854638f..aaa96012d 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -41,6 +41,7 @@ The following is a reference of the available data during book's parsing and the
| `page.title` | Title of the page |
| `page.previous` | Previous page in the Table of Contents (can be `null`) |
| `page.next` | Next page in the Table of Contents (can be `null`) |
+| `page.dir` | Text direction, based on configuration or detected from content (`rtl` or `ltr`) |
#### Table of Contents Variables
diff --git a/lib/page/index.js b/lib/page/index.js
index bc12529d3..d8b5fffb9 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -1,6 +1,7 @@
var _ = require('lodash');
var path = require('path');
var parsers = require('gitbook-parsers');
+var direction = require('direction');
var error = require('../utils/error');
var pathUtil = require('../utils/path');
@@ -101,6 +102,13 @@ Page.prototype.getContext = function() {
var next = article? article.next() : null;
var prev = article? article.prev() : null;
+ // Detect text direction in this page
+ var dir = this.book.config.get('direction');
+ if (!dir) {
+ dir = direction(this.content);
+ if (dir == 'neutral') dir = null;
+ }
+
return {
file: {
path: this.path,
@@ -112,7 +120,8 @@ Page.prototype.getContext = function() {
next: next? next.getContext() : null,
previous: prev? prev.getContext() : null,
level: article? article.level : null,
- content: this.content
+ content: this.content,
+ dir: dir
}
};
};
diff --git a/package.json b/package.json
index 5fceb40f9..da6c1a981 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,6 @@
"nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
- "i18n": "0.5.0",
"semver": "5.0.1",
"npmi": "0.1.1",
"cheerio": "0.19.0",
@@ -47,7 +46,8 @@
"deprecated": "0.0.1",
"rmdir": "1.2.0",
"cp": "0.2.0",
- "cpr": "1.0.0"
+ "cpr": "1.0.0",
+ "direction": "0.1.5"
},
"devDependencies": {
"eslint": "1.5.0",
diff --git a/test/page.js b/test/page.js
index 52d86d8e5..9afec1c0b 100644
--- a/test/page.js
+++ b/test/page.js
@@ -29,6 +29,8 @@ describe('Page', function() {
'variables/page/title.md': '{{ page.title }}',
'variables/page/previous.md': '{{ page.previous.title }} {{ page.previous.path }}',
'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}',
+ 'variables/page/dir/ltr.md': 'This is english: {{ page.dir }}',
+ 'variables/page/dir/rtl.md': 'بسيطة {{ page.dir }}',
'GLOSSARY.md': '# Glossary\n\n\n## abracadabra\n\nthis is the description'
}, [
@@ -312,6 +314,20 @@ describe('Page', function() {
return page.toHTML(output)
.should.be.fulfilledWith('Test Variables variables/page/title.md
\n');
});
+
+ describe('page.dir', function() {
+ it('should detect ltr', function() {
+ var page = book.addPage('variables/page/dir/ltr.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('This is english: ltr
\n');
+ });
+
+ it('should detect rtl', function() {
+ var page = book.addPage('variables/page/dir/rtl.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('بسيطة rtl
\n');
+ });
+ });
});
describe('Annotations / Glossary', function() {
From ea13e3cec45ad2b52f9346914331c43d91000d17 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 18 Feb 2016 19:06:23 +0100
Subject: [PATCH 112/217] Change default theme repository
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index da6c1a981..4189a645b 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
"gitbook-plugin-fontsettings": "1.0.2",
- "gitbook-plugin-theme-default": "git+https://github.com/GitbookIO/gitbook-theme-default.git#master",
+ "gitbook-plugin-theme-default": "git+https://github.com/GitbookIO/theme-default.git#master",
"nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
From f763e1dc4201f1593c1baf26697b175e96402442 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 18 Feb 2016 19:19:09 +0100
Subject: [PATCH 113/217] Lint after travis tests
---
.travis.yml | 2 ++
bin/gitbook.js | 1 +
gulpfile.js | 47 -------------------------------------------
lib/config/default.js | 2 --
lib/config/index.js | 6 ------
lib/output/index.js | 10 ---------
lib/template/index.js | 14 ++++++-------
package.json | 14 +++----------
test/mock.js | 2 ++
9 files changed, 15 insertions(+), 83 deletions(-)
delete mode 100644 gulpfile.js
delete mode 100644 lib/output/index.js
diff --git a/.travis.yml b/.travis.yml
index 6326f6795..8de84e728 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,3 +6,5 @@ node_js:
- "0.12"
before_install:
- npm install svgexport -g
+after_success:
+ - npm run lint
diff --git a/bin/gitbook.js b/bin/gitbook.js
index 5f474201b..5cadbc995 100755
--- a/bin/gitbook.js
+++ b/bin/gitbook.js
@@ -1,4 +1,5 @@
#! /usr/bin/env node
+/* eslint-disable no-console */
var color = require('bash-color');
diff --git a/gulpfile.js b/gulpfile.js
deleted file mode 100644
index 570b6a038..000000000
--- a/gulpfile.js
+++ /dev/null
@@ -1,47 +0,0 @@
-var _ = require('lodash');
-var gulp = require('gulp');
-var gutil = require('gulp-util');
-var less = require('gulp-less');
-var rename = require('gulp-rename');
-var minifyCSS = require('gulp-minify-css');
-var browserify = require('browserify');
-var mergeStream = require('merge-stream');
-var source = require('vinyl-source-stream');
-
-gulp.task('css', function() {
- var merged = mergeStream();
-
- _.each({
- 'ebook.less': 'ebook/ebook.css',
- 'pdf.less': 'ebook/pdf.css',
- 'mobi.less': 'ebook/mobi.css',
- 'epub.less': 'ebook/epub.css',
- 'website.less': 'website/style.css'
- }, function(out, input) {
- gutil.log('compiling', input, 'into', out);
- merged.add(gulp.src('theme/stylesheets/'+input)
- .pipe(less())
- .pipe(minifyCSS())
- .pipe(rename(out))
- .pipe(gulp.dest('theme/assets/')));
- });
-
- return merged;
-});
-
-gulp.task('js', function() {
- return browserify('./theme/javascript/index.js')
- .bundle()
- .pipe(source('app.js'))
- .pipe(gulp.dest('./theme/assets/website'));
-});
-
-gulp.task('assets', function() {
- return gulp.src('./node_modules/font-awesome/fonts/*')
- .pipe(gulp.dest('theme/assets/website/fonts/fontawesome/'));
-});
-
-gulp.task('default', ['css', 'js', 'assets'], function() {
-
-});
-
diff --git a/lib/config/default.js b/lib/config/default.js
index 1695e11ec..23a92f90e 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -1,5 +1,3 @@
-var path = require('path');
-
module.exports = {
// Book metadatas (somes are extracted from the README by default)
'title': null,
diff --git a/lib/config/index.js b/lib/config/index.js
index a5a0d67bb..34bf97c14 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -62,12 +62,6 @@ Config.prototype.load = function() {
that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
that.options.plugins = _.uniq(that.options.plugins, 'name');
- // Default value for text direction (from language)
- /*if (!that.options.direction) {
- var lang = i18n.getCatalog(that.options.language);
- if (lang) that.options.direction = lang.direction;
- }*/
-
that.options.gitbook = gitbook.version;
});
};
diff --git a/lib/output/index.js b/lib/output/index.js
deleted file mode 100644
index dcb2ffe1a..000000000
--- a/lib/output/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-var _ = require('lodash');
-//var EbookGenerator = require('./ebook');
-
-module.exports = {
- json: require('./json'),
- /*website: require('./website'),
- pdf: _.partialRight(EbookGenerator, 'pdf'),
- mobi: _.partialRight(EbookGenerator, 'mobi'),
- epub: _.partialRight(EbookGenerator, 'epub')*/
-};
diff --git a/lib/template/index.js b/lib/template/index.js
index bb8272c5b..d426988b8 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -176,7 +176,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
var args = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(tok.value);
- while (1) {
+ do {
// Read body
var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
@@ -193,14 +193,14 @@ TemplateEngine.prototype.addBlock = function(name, block) {
// Read new block
lastBlockName = parser.peekToken().value;
- if (lastBlockName == block.end) {
- break;
- }
// Parse signature and move to the end of the block
- lastBlockArgs = parser.parseSignature(null, true);
- parser.advanceAfterBlockEnd(lastBlockName);
- }
+ if (lastBlockName != block.end) {
+ lastBlockArgs = parser.parseSignature(null, true);
+ parser.advanceAfterBlockEnd(lastBlockName);
+ }
+ } while (lastBlockName == block.end)
+
parser.advanceAfterBlockEnd();
var bodies = [body];
diff --git a/package.json b/package.json
index 4189a645b..714952b7c 100644
--- a/package.json
+++ b/package.json
@@ -52,19 +52,11 @@
"devDependencies": {
"eslint": "1.5.0",
"mocha": "2.4.5",
- "should": "8.2.2",
- "gulp": "^3.8.11",
- "gulp-rename": "^1.2.2",
- "gulp-uglify": "1.1.0",
- "gulp-less": "3.0.2",
- "gulp-minify-css": "1.0.0",
- "gulp-util": "3.0.6",
- "browserify": "11.0.1",
- "merge-stream": "0.1.7",
- "vinyl-source-stream": "1.1.0"
+ "should": "8.2.2"
},
"scripts": {
- "test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js"
+ "test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js",
+ "lint": "eslint ."
},
"repository": {
"type": "git",
diff --git a/test/mock.js b/test/mock.js
index f9cc65789..b075f2c92 100644
--- a/test/mock.js
+++ b/test/mock.js
@@ -1,3 +1,5 @@
+/* eslint-disable no-console */
+
var Q = require('q');
var _ = require('lodash');
var tmp = require('tmp');
From 89378c3883140ce6e6c74e71651270f19b1ee441 Mon Sep 17 00:00:00 2001
From: Samy Pesse
Date: Thu, 18 Feb 2016 20:32:11 +0100
Subject: [PATCH 114/217] Update graceful-fs@4
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 714952b7c..c9001e953 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dependencies": {
"q": "1.0.1",
"lodash": "3.10.1",
- "graceful-fs": "3.0.5",
+ "graceful-fs": "4.1.3",
"resolve": "0.6.3",
"mkdirp": "0.5.1",
"error": "7.0.2",
From a22539a91eea4101d244cdaeb5c61e31376ff4bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 20 Feb 2016 16:02:51 +0100
Subject: [PATCH 115/217] Add new gitbook-markdown and gitbook-asciidoc
---
lib/book.js | 2 +-
lib/cli/watch.js | 2 +-
lib/page/index.js | 2 +-
lib/parsers.js | 60 +++++++++++++++++++++++++++++++++++++++++++
lib/template/index.js | 2 +-
lib/utils/promise.js | 12 +++++++++
package.json | 3 ++-
7 files changed, 78 insertions(+), 5 deletions(-)
create mode 100644 lib/parsers.js
diff --git a/lib/book.js b/lib/book.js
index 40c96861c..cbb85c0bb 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -1,7 +1,6 @@
var _ = require('lodash');
var path = require('path');
var Ignore = require('ignore');
-var parsers = require('gitbook-parsers');
var Config = require('./config');
var Readme = require('./backbone/readme');
@@ -13,6 +12,7 @@ var pathUtil = require('./utils/path');
var error = require('./utils/error');
var Promise = require('./utils/promise');
var Logger = require('./utils/logger');
+var parsers = require('./parsers');
/*
diff --git a/lib/cli/watch.js b/lib/cli/watch.js
index 36c42ccd1..b98faeb61 100644
--- a/lib/cli/watch.js
+++ b/lib/cli/watch.js
@@ -1,9 +1,9 @@
var _ = require('lodash');
var path = require('path');
var chokidar = require('chokidar');
-var parsers = require('gitbook-parsers');
var Promise = require('../utils/promise');
+var parsers = require('../parsers');
// Watch a folder and resolve promise once a file is modified
function watch(dir) {
diff --git a/lib/page/index.js b/lib/page/index.js
index d8b5fffb9..2bdc95177 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -1,11 +1,11 @@
var _ = require('lodash');
var path = require('path');
-var parsers = require('gitbook-parsers');
var direction = require('direction');
var error = require('../utils/error');
var pathUtil = require('../utils/path');
var location = require('../utils/location');
+var parsers = require('../parsers');
var HTMLPipeline = require('./html');
/*
diff --git a/lib/parsers.js b/lib/parsers.js
new file mode 100644
index 000000000..68998658d
--- /dev/null
+++ b/lib/parsers.js
@@ -0,0 +1,60 @@
+var _ = require('lodash');
+var path = require('path');
+
+var markdownParser = require('gitbook-markdown');
+var asciidocParser = require('gitbook-asciidoc');
+
+var Promise = require('./utils/promise');
+
+// This list is ordered by priority of parsers to use
+var PARSERS = [
+ createParser(markdownParser, {
+ name: 'markdown',
+ extensions: ['.md', '.markdown', '.mdown']
+ }),
+ createParser(asciidocParser, {
+ name: 'asciidoc',
+ extensions: ['.adoc', '.asciidoc']
+ })
+];
+
+
+// Prepare and compose a parser
+function createParser(parser, base) {
+ var nparser = base;
+
+ nparser.glossary = Promise.wrapfn(parser.glossary);
+ nparser.glossary.toText = Promise.wrapfn(parser.glossary.toText);
+
+ nparser.summary = Promise.wrapfn(parser.summary);
+ nparser.summary.toText = Promise.wrapfn(parser.summary.toText);
+
+ nparser.langs = Promise.wrapfn(parser.langs);
+ nparser.langs.toText = Promise.wrapfn(parser.langs.toText);
+
+ nparser.readme = Promise.wrapfn(parser.readme);
+
+ nparser.page = Promise.wrapfn(parser.page);
+ nparser.page.prepare = Promise.wrapfn(parser.page.prepare || _.identity);
+
+ return nparser;
+}
+
+// Return a specific parser according to an extension
+function getParser(ext) {
+ return _.find(PARSERS, function(input) {
+ return input.name == ext || _.contains(input.extensions, ext);
+ });
+}
+
+// Return parser for a file
+function getParserForFile(filename) {
+ return getParser(path.extname(filename));
+}
+
+module.exports = {
+ all: PARSERS,
+ extensions: _.flatten(_.pluck(PARSERS, 'extensions')),
+ get: getParser,
+ getForFile: getParserForFile
+};
diff --git a/lib/template/index.js b/lib/template/index.js
index d426988b8..215ebcde4 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -1,11 +1,11 @@
var _ = require('lodash');
var path = require('path');
var nunjucks = require('nunjucks');
-var parsers = require('gitbook-parsers');
var escapeStringRegexp = require('escape-string-regexp');
var Promise = require('../utils/promise');
var error = require('../utils/error');
+var parsers = require('../parsers');
var defaultBlocks = require('./blocks');
var Loader = require('./loader');
diff --git a/lib/utils/promise.js b/lib/utils/promise.js
index adcc1c3d4..d49cf27a2 100644
--- a/lib/utils/promise.js
+++ b/lib/utils/promise.js
@@ -43,8 +43,20 @@ function map(arr, iter) {
}, []);
}
+// Wrap a fucntion in a promise
+function wrap(func) {
+ return _.wrap(func, function(_func) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return Q()
+ .then(function() {
+ return _func.apply(null, args);
+ });
+ });
+}
+
module.exports = Q;
module.exports.reduce = reduce;
module.exports.map = map;
module.exports.serie = serie;
module.exports.some = some;
+module.exports.wrapfn = wrap;
diff --git a/package.json b/package.json
index c9001e953..7163f1546 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"resolve": "0.6.3",
"mkdirp": "0.5.1",
"error": "7.0.2",
- "gitbook-parsers": "0.8.9",
+ "gitbook-markdown": "1.0.0",
+ "gitbook-asciidoc": "1.0.0",
"gitbook-plugin-highlight": "1.0.3",
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
From cafbcd409774c383a26ce22b82b2104774c54c6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 20 Feb 2016 17:18:18 +0100
Subject: [PATCH 116/217] Complete normalization of summary with level and
multiples parts
---
lib/backbone/summary.js | 103 ++++++++++++++++++++++++++++++++--------
package.json | 4 +-
test/summary.js | 58 ++++++++++++++++++----
3 files changed, 135 insertions(+), 30 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 8e1a66d8b..d9253a785 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -20,7 +20,11 @@ function TOCArticle(def, parent) {
// As string indicating the overall position
// ex: '1.0.0'
- this.level = def.level;
+ this.level;
+
+ // When README has been automatically added
+ this.isAutoIntro = def.isAutoIntro;
+ this.isIntroduction = def.isIntroduction;
if (!def.title) {
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
@@ -41,10 +45,16 @@ function TOCArticle(def, parent) {
}
// Iterate over all articles in this articles
-TOCArticle.prototype.walk = function(iter) {
- _.each(this.articles, function(article) {
- iter(article);
- article.walk(iter);
+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);
});
};
@@ -69,6 +79,11 @@ 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;
@@ -147,19 +162,37 @@ TOCArticle.prototype.map = function(iter) {
A part of a ToC is a composed of a tree of articles.
*/
function TOCPart(part) {
+ if (!(this instanceof TOCPart)) return new TOCPart(part);
+
this.articles = _.map(part.articles || part.chapters, function(article) {
return new TOCArticle(article, this);
}, this);
}
// Iterate over all entries of the part
-TOCPart.prototype.walk = function(iter) {
- _.each(this.articles, function(article) {
- if (iter(article) === false) {
+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);
+ article.walk(iter, level);
});
};
@@ -187,15 +220,27 @@ Summary.prototype.parse = function(content) {
return this.parser.summary(content)
- // TODO: update GitBook's parsers to return a list of parts
- .then(function(part) {
- that.parts = [new TOCPart(part)];
+ .then(function(summary) {
+ that.parts = _.map(summary.parts, TOCPart);
- // Update count of articles
- that._length = 0;
- that.walk(function() {
- that._length += 1;
- });
+ // Create first part if none
+ if (that.parts.length == 0) {
+ that.parts.push(new TOCPart([]));
+ }
+
+ // 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 count of articles and create "level"
+ that.update();
});
};
@@ -224,8 +269,10 @@ Summary.prototype.getContext = function() {
// Iterate over all entries of the summary
// iter is called with an TOCArticle
Summary.prototype.walk = function(iter) {
- _.each(this.parts, function(part) {
- part.walk(iter);
+ var hasMultipleParts = this.parts.length > 1;
+
+ _.each(this.parts, function(part, i) {
+ part.walk(iter, hasMultipleParts? levelId('', i) : null);
});
};
@@ -264,4 +311,22 @@ Summary.prototype.count = function() {
return this._length;
};
+// Update the count and indexing of "level"
+Summary.prototype.update = function() {
+ var that = this;
+
+ that._length = 0;
+ that.walk(function(article, level) {
+ article.level = level;
+ 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;
diff --git a/package.json b/package.json
index 7163f1546..6b923b969 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,8 @@
"resolve": "0.6.3",
"mkdirp": "0.5.1",
"error": "7.0.2",
- "gitbook-markdown": "1.0.0",
- "gitbook-asciidoc": "1.0.0",
+ "gitbook-markdown": "1.0.2",
+ "gitbook-asciidoc": "1.0.1",
"gitbook-plugin-highlight": "1.0.3",
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
diff --git a/test/summary.js b/test/summary.js
index a9aea5dcf..8805c2da3 100644
--- a/test/summary.js
+++ b/test/summary.js
@@ -2,18 +2,32 @@ var should = require('should');
var mock = require('./mock');
+function mockSummary(files, summary) {
+ return mock.setupDefaultBook(files, summary)
+ .then(function(book) {
+ return book.readme.load()
+ .then(function() {
+ return book.summary.load();
+ })
+ .thenResolve(book);
+ });
+}
+
describe('Summary / Table of contents', function() {
describe('Empty summary list', function() {
var book;
before(function() {
- return mock.setupDefaultBook({})
+ return mockSummary({})
.then(function(_book) {
book = _book;
- return book.summary.load();
});
});
+ it('should add README as first entry', function() {
+ should(book.summary.getArticle('README.md')).be.ok();
+ });
+
it('should correctly count articles', function() {
book.summary.count().should.equal(1);
});
@@ -23,14 +37,13 @@ describe('Summary / Table of contents', function() {
var book;
before(function() {
- return mock.setupDefaultBook({
+ return mockSummary({
'SUMMARY.md': '# Summary\n\n'
+ '* [Hello](./hello.md)\n'
+ '* [World](./world.md)\n\n'
})
.then(function(_book) {
book = _book;
- return book.summary.load();
});
});
@@ -39,11 +52,40 @@ describe('Summary / Table of contents', function() {
});
});
+ describe('Levels', function() {
+ var book;
+
+ before(function() {
+ return mockSummary({
+ 'SUMMARY.md': '# Summary\n\n'
+ + '* [Hello](./hello.md)\n'
+ + ' * [Hello 2](./hello2.md)\n'
+ + '* [World](./world.md)\n\n'
+ + '## Part 2\n\n'
+ + '* [Hello 3](./hello.md)\n'
+ + ' * [Hello 4](./hello2.md)\n'
+ })
+ .then(function(_book) {
+ book = _book;
+ });
+ });
+
+ it('should correctly index levels', function() {
+ book.summary.getArticleByLevel('0').title.should.equal('Introduction');
+ book.summary.getArticleByLevel('1.1').title.should.equal('Hello');
+ book.summary.getArticleByLevel('1.1.1').title.should.equal('Hello 2');
+ book.summary.getArticleByLevel('1.2').title.should.equal('World');
+
+ book.summary.getArticleByLevel('2.1').title.should.equal('Hello 3');
+ book.summary.getArticleByLevel('2.1.1').title.should.equal('Hello 4');
+ });
+ });
+
describe('External', function() {
var book;
before(function() {
- return mock.setupDefaultBook({}, [
+ return mockSummary({}, [
{
title: 'Google',
path: 'https://www.google.fr'
@@ -51,7 +93,6 @@ describe('Summary / Table of contents', function() {
])
.then(function(_book) {
book = _book;
- return book.summary.load();
});
});
@@ -66,7 +107,7 @@ describe('Summary / Table of contents', function() {
should(article.path).not.be.ok();
article.title.should.equal('Google');
- article.ref.should.equal('https:/www.google.fr');
+ article.ref.should.equal('https://www.google.fr');
article.isExternal().should.be.ok;
});
});
@@ -75,7 +116,7 @@ describe('Summary / Table of contents', function() {
var book;
before(function() {
- return mock.setupDefaultBook({
+ return mockSummary({
'SUMMARY.md': '# Summary\n\n' +
'* [Hello](hello.md)\n' +
'* [Hello 2](hello2.md)\n' +
@@ -86,7 +127,6 @@ describe('Summary / Table of contents', function() {
})
.then(function(_book) {
book = _book;
- return book.summary.load();
});
});
From 847c6b9aa2a84f1a7508341a6e64230c8a3daa1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 20 Feb 2016 17:32:32 +0100
Subject: [PATCH 117/217] Fix error in blocks detection with nunjucks
---
lib/template/index.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lib/template/index.js b/lib/template/index.js
index 215ebcde4..a2d00b543 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -199,7 +199,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
lastBlockArgs = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(lastBlockName);
}
- } while (lastBlockName == block.end)
+ } while (lastBlockName != block.end)
parser.advanceAfterBlockEnd();
@@ -343,6 +343,7 @@ TemplateEngine.prototype.render = function(content, context, options) {
options = _.defaults(options || {}, {
path: null
});
+ var filename = options.path;
// Setup path and type
if (options.path) {
@@ -355,7 +356,7 @@ TemplateEngine.prototype.render = function(content, context, options) {
return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options)
.fail(function(err) {
throw error.TemplateError(err, {
- filename: options.path
+ filename: filename || ''
});
});
};
From fb921788b91b4f78c3ec932727a92c5ecda00144 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 20 Feb 2016 17:35:10 +0100
Subject: [PATCH 118/217] Fix page.toHTML by using .content directly
---
lib/page/index.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/lib/page/index.js b/lib/page/index.js
index 2bdc95177..c4d2721d4 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -152,8 +152,7 @@ Page.prototype.toHTML = function(output) {
.then(function() {
return that.parser.page(that.content)
.then(function(out) {
- var content = _.pluck(out.sections, 'content').join('\n');
- that.update(content);
+ that.update(out.content);
});
})
From 42bb5fccb81ea9ce2b406b32502236d0f34e2e7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sat, 20 Feb 2016 17:39:11 +0100
Subject: [PATCH 119/217] Change glossary to enforce use of h2
---
test/glossary.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/glossary.js b/test/glossary.js
index 0efef34c1..e1ba82a92 100644
--- a/test/glossary.js
+++ b/test/glossary.js
@@ -23,7 +23,7 @@ describe('Glossary', function() {
before(function() {
return mock.setupDefaultBook({
- 'GLOSSARY.md': '# Glossary\n\n### Hello World\n\nThis is an entry'
+ 'GLOSSARY.md': '# Glossary\n\n## Hello World\n\nThis is an entry'
})
.then(function(_book) {
book = _book;
From 98e9848c4a5260800376255e00c58bec029a1565 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 00:28:59 +0100
Subject: [PATCH 120/217] Add title to summary's parts
---
lib/backbone/summary.js | 4 +++-
package.json | 4 ++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index d9253a785..f3994caa9 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -164,6 +164,7 @@ A part of a ToC is a composed of a tree of articles.
function TOCPart(part) {
if (!(this instanceof TOCPart)) return new TOCPart(part);
+ this.title = part.title;
this.articles = _.map(part.articles || part.chapters, function(article) {
return new TOCArticle(article, this);
}, this);
@@ -225,7 +226,7 @@ Summary.prototype.parse = function(content) {
// Create first part if none
if (that.parts.length == 0) {
- that.parts.push(new TOCPart([]));
+ that.parts.push(new TOCPart({}));
}
// Add README as first entry
@@ -259,6 +260,7 @@ Summary.prototype.getContext = function() {
summary: {
parts: _.map(this.parts, function(part) {
return {
+ title: part.title,
articles: part.map(onArticle)
};
})
diff --git a/package.json b/package.json
index 6b923b969..faed0facc 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,8 @@
"resolve": "0.6.3",
"mkdirp": "0.5.1",
"error": "7.0.2",
- "gitbook-markdown": "1.0.2",
- "gitbook-asciidoc": "1.0.1",
+ "gitbook-markdown": "1.0.3",
+ "gitbook-asciidoc": "1.0.2",
"gitbook-plugin-highlight": "1.0.3",
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
From 0a93ffd76ba574de5afaad73954c287ae40143d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 00:33:10 +0100
Subject: [PATCH 121/217] Ignore node_modules during login
---
lib/output/base.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/lib/output/base.js b/lib/output/base.js
index 2a7ec488e..f5a8e4fb6 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -49,11 +49,13 @@ Output.prototype.generate = function() {
// Initialize the generation
.then(function() {
+ that.log.debug.ln('preparing the generation');
return that.prepare();
})
// Process all files
.then(function() {
+ that.log.debug.ln('listing files');
return that.book.fs.listAllFiles(that.book.root);
})
@@ -89,6 +91,7 @@ Output.prototype.generate = function() {
// Finish the generation
.then(function() {
+ that.log.debug.ln('finishing the generation');
return that.finish();
})
@@ -103,6 +106,7 @@ Output.prototype.prepare = function() {
'.gitignore',
'.ignore',
'.bookignore',
+ 'node_modules',
// The configuration file should not be copied in the output
this.book.config.path,
From b4cdbc6534b12d4547898077b7d5b2fd2b2163d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 00:42:11 +0100
Subject: [PATCH 122/217] Add custom filters for dates to templating engine
---
lib/output/folder.js | 2 ++
lib/template/filters.js | 15 +++++++++++++++
lib/template/index.js | 4 +++-
package.json | 3 ++-
4 files changed, 22 insertions(+), 2 deletions(-)
create mode 100644 lib/template/filters.js
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 9e139c0d9..4d8584edc 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -39,6 +39,7 @@ module.exports = function folderOutput(Base) {
// Cleanup output folder
.then(function() {
+ that.log.debug.ln('removing previous output directory');
return fs.rmDir(that.root())
.fail(function() {
return Promise();
@@ -47,6 +48,7 @@ module.exports = function folderOutput(Base) {
// Create output folder
.then(function() {
+ that.log.debug.ln('creating output directory');
return fs.mkdirp(that.root());
})
diff --git a/lib/template/filters.js b/lib/template/filters.js
new file mode 100644
index 000000000..ac68b82ac
--- /dev/null
+++ b/lib/template/filters.js
@@ -0,0 +1,15 @@
+var moment = require('moment');
+
+
+module.exports = {
+ // Format a date
+ // ex: 'MMMM Do YYYY, h:mm:ss a
+ date: function(time, format) {
+ return moment(time).format(format);
+ },
+
+ // Relative Time
+ dateFromNow: function(time) {
+ return moment(time).fromNow();
+ }
+};
diff --git a/lib/template/index.js b/lib/template/index.js
index a2d00b543..a130088a1 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -7,6 +7,7 @@ var Promise = require('../utils/promise');
var error = require('../utils/error');
var parsers = require('../parsers');
var defaultBlocks = require('./blocks');
+var defaultFilters = require('./filters');
var Loader = require('./loader');
// Return extension name for a specific block
@@ -59,8 +60,9 @@ function TemplateEngine(output) {
// Bind methods
_.bindAll(this);
- // Add default blocks
+ // Add default blocks and filters
this.addBlocks(defaultBlocks);
+ this.addFilters(defaultFilters);
}
// Bind a function to a context
diff --git a/package.json b/package.json
index faed0facc..a5905c48e 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,8 @@
"rmdir": "1.2.0",
"cp": "0.2.0",
"cpr": "1.0.0",
- "direction": "0.1.5"
+ "direction": "0.1.5",
+ "moment": "2.11.2"
},
"devDependencies": {
"eslint": "1.5.0",
From 2f15e26ffa9ca67007e9f23ac732fed0fcb109d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 01:02:20 +0100
Subject: [PATCH 123/217] Fix context for templating and page
---
lib/output/base.js | 12 ------------
lib/output/json.js | 2 +-
lib/output/website.js | 2 +-
lib/page/index.js | 36 ++++++++++++++++++++++--------------
4 files changed, 24 insertions(+), 28 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index f5a8e4fb6..41905597d 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -175,18 +175,6 @@ Output.prototype.onResolveTemplate = function(from, to) {
// ---- Utilities ----
-// Return the complete templating context for a page
-Output.prototype.getPageContext = function(page) {
- return _.extend(
- page.getContext(),
- gitbook.getContext(),
- this.book.getContext(),
- this.book.summary.getContext(),
- this.book.glossary.getContext(),
- this.book.config.getContext()
- );
-};
-
// Resolve a file path in the context of a specific page
// Result is an "absolute path relative to the output folder"
Output.prototype.resolveForPage = function(page, href) {
diff --git a/lib/output/json.js b/lib/output/json.js
index 58055799a..86a230758 100644
--- a/lib/output/json.js
+++ b/lib/output/json.js
@@ -16,7 +16,7 @@ JSONOutput.prototype.onPage = function(page) {
// Write as json
.then(function() {
- var json = that.getPageContext(page);
+ var json = page.getContext();
// Delete some private properties
delete json.config;
diff --git a/lib/output/website.js b/lib/output/website.js
index d85622972..1cdd4a689 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -138,7 +138,7 @@ WebsiteOutput.prototype.onPage = function(page) {
// Render the page template with the same context as the json output
.then(function() {
- return that.render('page', that.getPageContext(page));
+ return that.render('page', page.getContext());
})
// Write the HTML file
diff --git a/lib/page/index.js b/lib/page/index.js
index c4d2721d4..45b35982b 100644
--- a/lib/page/index.js
+++ b/lib/page/index.js
@@ -6,6 +6,7 @@ var error = require('../utils/error');
var pathUtil = require('../utils/path');
var location = require('../utils/location');
var parsers = require('../parsers');
+var gitbook = require('../gitbook');
var HTMLPipeline = require('./html');
/*
@@ -109,21 +110,28 @@ Page.prototype.getContext = function() {
if (dir == 'neutral') dir = null;
}
- return {
- file: {
- path: this.path,
- mtime: this.mtime,
- type: this.type
+ return _.extend(
+ {
+ file: {
+ path: this.path,
+ mtime: this.mtime,
+ type: this.type
+ },
+ page: {
+ title: article? article.title : null,
+ next: next? next.getContext() : null,
+ previous: prev? prev.getContext() : null,
+ level: article? article.level : null,
+ content: this.content,
+ dir: dir
+ }
},
- page: {
- title: article? article.title : null,
- next: next? next.getContext() : null,
- previous: prev? prev.getContext() : null,
- level: article? article.level : null,
- content: this.content,
- dir: dir
- }
- };
+ gitbook.getContext(),
+ this.book.getContext(),
+ this.book.summary.getContext(),
+ this.book.glossary.getContext(),
+ this.book.config.getContext()
+ );
};
// Parse the page and return its content
From e16b3e35f438014cb51fac84a500c8996d18658d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 01:02:38 +0100
Subject: [PATCH 124/217] Setup summary for documentation
---
.gitignore | 3 +++
book.js | 14 --------------
docs/README.md | 2 +-
docs/SUMMARY.md | 19 ++++++++++++++++++-
docs/book.js | 11 +++++++++++
docs/plugins.md | 0
6 files changed, 33 insertions(+), 16 deletions(-)
delete mode 100644 book.js
create mode 100644 docs/book.js
create mode 100644 docs/plugins.md
diff --git a/.gitignore b/.gitignore
index 79d58fcc4..a7a8f664e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,9 @@ node_modules
# vim swapfile
*.swp
+# Output of documentation
+docs/_book
+
lib2
test2
theme2
\ No newline at end of file
diff --git a/book.js b/book.js
deleted file mode 100644
index 773256ad8..000000000
--- a/book.js
+++ /dev/null
@@ -1,14 +0,0 @@
-var pkg = require('./package.json');
-
-module.exports = {
- root: './docs',
- plugins: ['versions'],
- pluginsConfig: {
- versions: {
- type: 'tags'
- }
- },
- variables: {
- version: pkg.version
- }
-};
diff --git a/docs/README.md b/docs/README.md
index 2dfffba31..8329a5cf9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,4 +1,4 @@
-# GitBook v{{ book.version }}: Documentation
+# GitBook Documentation: v{{ book.version }}
### FAQ
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 58d67636a..a4f2a98d2 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -1,13 +1,30 @@
# Summary
+### Getting Started
+
* [About this document](README.md)
+* [Installation and Setup](setup.md)
+* [Directory structure](structure.md)
+
+### Your Content
+
* [Configuration](config.md)
* [Pages and Summary](pages.md)
* [Assets](assets.md)
* [Glossary](glossary.md)
+
+### Miscellaneous
+
* [Templating](templating.md)
* [Content References](conrefs.md)
* [Variables](variables.md)
* [Filters](filters.md)
+
+### Customization
+
* [Plugins](plugins.md)
-* [Theming](themes.md)
\ No newline at end of file
+* [Theming](themes.md)
+
+--
+
+* [FAQ](faq.md)
diff --git a/docs/book.js b/docs/book.js
new file mode 100644
index 000000000..b04e91cda
--- /dev/null
+++ b/docs/book.js
@@ -0,0 +1,11 @@
+var pkg = require('../package.json');
+
+module.exports = {
+ "title": "GitBook Documentation",
+
+ plugins: ["theme-official"],
+ theme: "official",
+ variables: {
+ version: pkg.version
+ }
+};
diff --git a/docs/plugins.md b/docs/plugins.md
new file mode 100644
index 000000000..e69de29bb
From 40f829ce54d6fe8c56c1005ed7229d198918acb9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 11:01:49 +0100
Subject: [PATCH 125/217] Add docs for setup gitbook
---
docs/SUMMARY.md | 7 ++++---
docs/faq.md | 3 ++-
docs/setup.md | 37 +++++++++++++++++++++++++++++++++++++
docs/structure.md | 26 ++++++++++++++++++++++++++
4 files changed, 69 insertions(+), 4 deletions(-)
create mode 100644 docs/setup.md
create mode 100644 docs/structure.md
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index a4f2a98d2..247fc27af 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -4,14 +4,15 @@
* [About this document](README.md)
* [Installation and Setup](setup.md)
-* [Directory structure](structure.md)
### Your Content
-* [Configuration](config.md)
+* [Directory structure](structure.md)
* [Pages and Summary](pages.md)
-* [Assets](assets.md)
+* [Assets and Images](assets.md)
* [Glossary](glossary.md)
+* [Multi-Lingual](langs.md)
+* [Configuration](config.md)
### Miscellaneous
diff --git a/docs/faq.md b/docs/faq.md
index 592fbdc3b..f9604096f 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -4,5 +4,6 @@
You should always use `.md` extensions for your relative links, GitBook will automatically replace these links by the right value when the pointing file is referenced in the Table of Contents.
+#### Can I create a GitBook in a sub-directory of my repository?
-
+Yes, GitBooks can be created in sub-directories. GitBook.com and the CLI also looks by default in a series of [folders](structure.md).
\ No newline at end of file
diff --git a/docs/setup.md b/docs/setup.md
new file mode 100644
index 000000000..aaa040717
--- /dev/null
+++ b/docs/setup.md
@@ -0,0 +1,37 @@
+# Setup and Installation of GitBook
+
+Getting GitBook installed and ready-to-go should only take a few minutes.
+
+### GitBook.com
+
+[GitBook.com](https://www.gitbook.com) is an easy to use solution to write, publish and host books. It's best and easier solution for publishing your content and collaborate on it.
+
+It integrates well with the [GitBook Editor](https://www.gitbook.com/editor).
+
+### Local Installation
+
+##### Requirements
+
+Installing GitBook is easy and straight-forward, but there are a few requirements you’ll need to make sure your system has before you start.
+
+* NodeJS (v4.0.0 and above are adviced)
+* Windows, Linux, Unix, or Mac OS X
+
+##### Install with NPM
+
+The best way to install GitBook is via **NPM**. At the terminal prompt, simply run the following command to install GitBook:
+
+```
+$ npm install gitbook-cli -g
+```
+
+`gitbook-cli` is an utility to install and use multiple versions of GitBook on the same system. It will automatically install the required version to build a book.
+
+##### Using pre-releases
+
+`gitbook-cli` makes it easy to install and test other versions of GitBook with your book:
+
+```
+$ gitbook install beta
+```
+
diff --git a/docs/structure.md b/docs/structure.md
new file mode 100644
index 000000000..f6ae6174d
--- /dev/null
+++ b/docs/structure.md
@@ -0,0 +1,26 @@
+# Directory structure
+
+
+```
+.
+├── book.json
+├── README.md
+├── SUMMARY.md
+├── chapter-1/
+| ├── README.md
+| └── something.md
+└── chapter-2/
+ ├── README.md
+ └── something.md
+```
+
+An overview of what each of these does:
+
+| File | Description |
+| -------- | ----------- |
+| `book.json` | Stores [configuration](config.md) data (__optional__) |
+| `README.md` | Preface / Introduction for your book (**required**) |
+| `SUMMARY.md` | Table of Contents |
+
+
+### Sub-directory
From 8df18746face0bf43d18540d3de5ac0cdd795549 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 11:22:41 +0100
Subject: [PATCH 126/217] Add debug log when copying theme
---
lib/output/website.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/output/website.js b/lib/output/website.js
index 1cdd4a689..bbb3fdc8e 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -74,7 +74,7 @@ WebsiteOutput.prototype.prepare = function() {
that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(searchPaths));
- that.env.addGlobal('__', function(s) {
+ that.env.addFilter('t', function(s) {
// todo: i18n
return s;
});
@@ -116,6 +116,7 @@ WebsiteOutput.prototype.prepare = function() {
var assetFolder = path.join(folder, '_assets', that.name);
if (!fs.existsSync(assetFolder)) return;
+ that.log.debug.ln('copy assets from theme', assetFolder);
return fs.copyDir(
assetFolder,
that.resolve('gitbook'),
From ca1313e891f7cfbdffaf6a47bb75a8bc1d77c1a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 11:28:55 +0100
Subject: [PATCH 127/217] Extend configuration with infos from readme
---
lib/book.js | 8 ++++++--
test/page.js | 8 ++++++++
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/lib/book.js b/lib/book.js
index cbb85c0bb..c81d2554b 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -196,9 +196,13 @@ Book.prototype.parse = function() {
// Parse the readme
.then(that.readme.load)
.then(function() {
- if (that.readme.exists()) return;
+ if (!that.readme.exists()) {
+ throw new error.FileNotFoundError({ filename: 'README' });
+ }
- throw new error.FileNotFoundError({ filename: 'README' });
+ // Default configuration to infos extracted from readme
+ if (!that.config.get('title')) that.config.set('title', that.readme.title);
+ if (!that.config.get('description')) that.config.set('description', that.readme.description);
})
// Parse the summary
diff --git a/test/page.js b/test/page.js
index 9afec1c0b..8eb209649 100644
--- a/test/page.js
+++ b/test/page.js
@@ -6,6 +6,7 @@ describe('Page', function() {
before(function() {
return mock.setupDefaultBook({
+ 'README.md': ' # Hello World\n\nThis is a description',
'heading.md': '# Hello\n\n## World',
'links.md': '[link](hello.md) [link 2](variables/page/next.md) [readme](README.md)',
@@ -31,6 +32,7 @@ describe('Page', function() {
'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}',
'variables/page/dir/ltr.md': 'This is english: {{ page.dir }}',
'variables/page/dir/rtl.md': 'بسيطة {{ page.dir }}',
+ 'variables/book/title.md': '{{ book.title}}',
'GLOSSARY.md': '# Glossary\n\n\n## abracadabra\n\nthis is the description'
}, [
@@ -315,6 +317,12 @@ describe('Page', function() {
.should.be.fulfilledWith('Test Variables variables/page/title.md
\n');
});
+ it('should set book.title', function() {
+ var page = book.addPage('variables/book/title.md');
+ return page.toHTML(output)
+ .should.be.fulfilledWith('Hello World
\n');
+ });
+
describe('page.dir', function() {
it('should detect ltr', function() {
var page = book.addPage('variables/page/dir/ltr.md');
From 884540e56530b8e1c68c94604c0b45f0489d5020 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 11:56:14 +0100
Subject: [PATCH 128/217] Fix .next and .prev for summary's articles to use
parts
---
lib/backbone/summary.js | 119 ++++++++++++++++++++++++++++++----------
test/summary.js | 37 +++++++++++--
2 files changed, 122 insertions(+), 34 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index f3994caa9..5cc14180d 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -8,8 +8,9 @@ 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)
+ 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
@@ -26,16 +27,17 @@ function TOCArticle(def, parent) {
this.isAutoIntro = def.isAutoIntro;
this.isIntroduction = def.isIntroduction;
- if (!def.title) {
- throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
- }
+ this.validate();
- var parts = url.parse(def.path);
+ // 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;
+ if (!this.isExternal()) {
+ this.path = parts.pathname;
+ this.anchor = parts.hash;
+ }
}
this.articles = _.map(def.articles || [], function(article) {
@@ -44,6 +46,13 @@ function TOCArticle(def, parent) {
}, 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;
@@ -91,11 +100,29 @@ TOCArticle.prototype.hasChildren = function() {
// Return true if has an article as parent
TOCArticle.prototype.hasParent = function() {
- return (this.parent instanceof TOCArticle);
+ return !(this.parent instanceof TOCPart);
+};
+
+// Return true if has a part as parent
+TOCArticle.prototype.hasParentPart = function() {
+ return (this.parent instanceof TOCPart);
+};
+
+// Return first article
+TOCArticle.prototype.first = function() {
+ return _.first(this.articles);
+};
+
+// Return last article
+TOCArticle.prototype.last = function() {
+ var last = _.last(this.articles);
+ if (!last) return null;
+
+ return last.last() || last;
};
// Return a sibling (next or prev) in the parent
-// Withotu taking in consideration children/parent
+// Without taking in consideration children/parent
TOCArticle.prototype.sibling = function(direction) {
var parentsArticles = this.parent.articles;
var pos = _.findIndex(parentsArticles, this);
@@ -115,17 +142,28 @@ TOCArticle.prototype._sibling = function(direction) {
return _.first(this.articles);
}
+ var part;
var parentsArticles = this.parent.articles;
var pos = _.findIndex(parentsArticles, this);
// First child and has parent
- if (pos == 0 && direction < 0 && this.hasParent()) {
- return this.parent;
+ if (pos == 0 && direction < 0) {
+ if (this.hasParent()) {
+ return this.parent;
+ } else if (this.hasParentPart()) {
+ part = this.parent.sibling(-1);
+ return part? part.last() : null;
+ }
}
// Last child and has parent
- if(pos == (parentsArticles.length - 1) && direction > 0 && this.hasParent()) {
- return this.parent.sibling(1);
+ if(pos == (parentsArticles.length - 1) && direction > 0) {
+ if (this.hasParent()) {
+ return this.parent.sibling(1);
+ } else if (this.hasParentPart()) {
+ part = this.parent.sibling(1);
+ return part? part.first() : null;
+ }
}
if (parentsArticles[pos + direction]) {
@@ -159,16 +197,31 @@ TOCArticle.prototype.map = function(iter) {
/*
-A part of a ToC is a composed of a tree of articles.
+ A part of a ToC is a composed of a tree of articles and an optiona title
*/
-function TOCPart(part) {
+function TOCPart(part, summary) {
if (!(this instanceof TOCPart)) return new TOCPart(part);
- this.title = part.title;
- this.articles = _.map(part.articles || part.chapters, function(article) {
- return new TOCArticle(article, this);
- }, this);
+ TOCArticle.apply(this, arguments);
+
+ this.summary = summary;
}
+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.summary.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) {
@@ -197,9 +250,16 @@ TOCPart.prototype.walk = function(iter, base) {
});
};
-// Map over all articles
-TOCPart.prototype.map = function(iter) {
- return _.map(this.articles, iter);
+// 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)
+ };
};
/*
@@ -222,11 +282,13 @@ Summary.prototype.parse = function(content) {
return this.parser.summary(content)
.then(function(summary) {
- that.parts = _.map(summary.parts, TOCPart);
+ that.parts = _.map(summary.parts, function(part) {
+ return new TOCPart(part, that);
+ });
// Create first part if none
if (that.parts.length == 0) {
- that.parts.push(new TOCPart({}));
+ that.parts.push(new TOCPart({}, that));
}
// Add README as first entry
@@ -259,10 +321,7 @@ Summary.prototype.getContext = function() {
return {
summary: {
parts: _.map(this.parts, function(part) {
- return {
- title: part.title,
- articles: part.map(onArticle)
- };
+ return part.getContext(onArticle);
})
}
};
diff --git a/test/summary.js b/test/summary.js
index 8805c2da3..f7a975f92 100644
--- a/test/summary.js
+++ b/test/summary.js
@@ -123,7 +123,10 @@ describe('Summary / Table of contents', function() {
' * [Hello 3](hello3.md)\n' +
' * [Hello 4](hello4.md)\n' +
' * [Hello 5](hello5.md)\n' +
- '* [Hello 6](hello6.md)\n'
+ '* [Hello 6](hello6.md)\n\n\n' +
+ '### Part 2\n\n' +
+ '* [Hello 7](hello7.md)\n' +
+ '* [Hello 8](hello8.md)\n\n'
})
.then(function(_book) {
book = _book;
@@ -136,7 +139,7 @@ describe('Summary / Table of contents', function() {
var prev = article.prev();
var next = article.next();
- should(prev).equal(null);
+ should(prev).not.be.ok();
should(next).be.ok();
next.path.should.equal('hello.md');
@@ -207,16 +210,42 @@ describe('Summary / Table of contents', function() {
next.path.should.equal('hello6.md');
});
- it('should return prev for last', function() {
+ it('should return next/prev for a joint <- parts', function() {
+ var article = book.summary.getArticle('hello7.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello6.md');
+ next.path.should.equal('hello8.md');
+ });
+
+ it('should return next/prev for a joint -> parts', function() {
var article = book.summary.getArticle('hello6.md');
var prev = article.prev();
var next = article.next();
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello5.md');
+ next.path.should.equal('hello7.md');
+ });
+
+ it('should return only prev for last', function() {
+ var article = book.summary.getArticle('hello8.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
should(prev).be.ok();
should(next).be.not.ok();
- prev.path.should.equal('hello5.md');
+ prev.path.should.equal('hello7.md');
});
});
});
From 51fd8ae08c1616b6826e72dd86e658f4e921bed9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 12:05:31 +0100
Subject: [PATCH 129/217] Simplify .next and .prev for articles
---
lib/backbone/summary.js | 92 ++++++-----------------------------------
test/summary.js | 48 ++++++++++++++-------
2 files changed, 45 insertions(+), 95 deletions(-)
diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js
index 5cc14180d..4cfff2847 100644
--- a/lib/backbone/summary.js
+++ b/lib/backbone/summary.js
@@ -22,6 +22,8 @@ function TOCArticle(def, 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;
@@ -103,91 +105,14 @@ TOCArticle.prototype.hasParent = function() {
return !(this.parent instanceof TOCPart);
};
-// Return true if has a part as parent
-TOCArticle.prototype.hasParentPart = function() {
- return (this.parent instanceof TOCPart);
-};
-
-// Return first article
-TOCArticle.prototype.first = function() {
- return _.first(this.articles);
-};
-
-// Return last article
-TOCArticle.prototype.last = function() {
- var last = _.last(this.articles);
- if (!last) return null;
-
- return last.last() || last;
-};
-
-// Return a sibling (next or prev) in the parent
-// Without taking in consideration children/parent
-TOCArticle.prototype.sibling = function(direction) {
- var parentsArticles = this.parent.articles;
- var pos = _.findIndex(parentsArticles, this);
-
- if (parentsArticles[pos + direction]) {
- return parentsArticles[pos + direction];
- }
-
- return null;
-};
-
-// Return a sibling (next or prev)
-// It takes parents.children in consideration
-TOCArticle.prototype._sibling = function(direction) {
- // Next should go to the first children
- if (direction > 0 && this.hasChildren()) {
- return _.first(this.articles);
- }
-
- var part;
- var parentsArticles = this.parent.articles;
- var pos = _.findIndex(parentsArticles, this);
-
- // First child and has parent
- if (pos == 0 && direction < 0) {
- if (this.hasParent()) {
- return this.parent;
- } else if (this.hasParentPart()) {
- part = this.parent.sibling(-1);
- return part? part.last() : null;
- }
- }
-
- // Last child and has parent
- if(pos == (parentsArticles.length - 1) && direction > 0) {
- if (this.hasParent()) {
- return this.parent.sibling(1);
- } else if (this.hasParentPart()) {
- part = this.parent.sibling(1);
- return part? part.first() : null;
- }
- }
-
- if (parentsArticles[pos + direction]) {
- var article = parentsArticles[pos + direction];
-
- // If goign back, take last children from "brother"
- if (direction < 0 && article.hasChildren()) {
- article = _.last(article.articles);
- }
-
- return article;
- }
-
- return null;
-};
-
// Return next article in the TOC
TOCArticle.prototype.next = function() {
- return this._sibling(1);
+ return this._next;
};
// Return previous article in the TOC
TOCArticle.prototype.prev = function() {
- return this._sibling(-1);
+ return this._prev;
};
// Map over all articles
@@ -375,10 +300,19 @@ Summary.prototype.count = function() {
// Update the count and indexing of "level"
Summary.prototype.update = function() {
var that = this;
+ 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;
});
};
diff --git a/test/summary.js b/test/summary.js
index f7a975f92..68c23b49d 100644
--- a/test/summary.js
+++ b/test/summary.js
@@ -126,7 +126,10 @@ describe('Summary / Table of contents', function() {
'* [Hello 6](hello6.md)\n\n\n' +
'### Part 2\n\n' +
'* [Hello 7](hello7.md)\n' +
- '* [Hello 8](hello8.md)\n\n'
+ ' * [Hello 8](hello8.md)\n\n' +
+ '### Part 3\n\n' +
+ '* [Hello 9](hello9.md)\n' +
+ '* [Hello 10](hello10.md)\n\n'
})
.then(function(_book) {
book = _book;
@@ -210,19 +213,6 @@ describe('Summary / Table of contents', function() {
next.path.should.equal('hello6.md');
});
- it('should return next/prev for a joint <- parts', function() {
- var article = book.summary.getArticle('hello7.md');
-
- var prev = article.prev();
- var next = article.next();
-
- should(prev).be.ok();
- should(next).be.ok();
-
- prev.path.should.equal('hello6.md');
- next.path.should.equal('hello8.md');
- });
-
it('should return next/prev for a joint -> parts', function() {
var article = book.summary.getArticle('hello6.md');
@@ -236,16 +226,42 @@ describe('Summary / Table of contents', function() {
next.path.should.equal('hello7.md');
});
- it('should return only prev for last', function() {
+ it('should return next/prev for a joint <- parts', function() {
+ var article = book.summary.getArticle('hello7.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello6.md');
+ next.path.should.equal('hello8.md');
+ });
+
+ it('should return next and prev', function() {
var article = book.summary.getArticle('hello8.md');
var prev = article.prev();
var next = article.next();
+ should(prev).be.ok();
+ should(next).be.ok();
+
+ prev.path.should.equal('hello7.md');
+ next.path.should.equal('hello9.md');
+ });
+
+ it('should return only prev for last', function() {
+ var article = book.summary.getArticle('hello10.md');
+
+ var prev = article.prev();
+ var next = article.next();
+
should(prev).be.ok();
should(next).be.not.ok();
- prev.path.should.equal('hello7.md');
+ prev.path.should.equal('hello9.md');
});
});
});
From e5dd987d038e93d424a65038046723531c2519b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 13:29:17 +0100
Subject: [PATCH 130/217] Add i18n filter fro themes
---
lib/output/website.js | 19 +++++++++++++++----
package.json | 3 ++-
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/lib/output/website.js b/lib/output/website.js
index bbb3fdc8e..32647f6c6 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -2,6 +2,7 @@ var _ = require('lodash');
var path = require('path');
var util = require('util');
var nunjucks = require('nunjucks');
+var I18n = require('i18n-t');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
@@ -29,6 +30,9 @@ function _WebsiteOutput() {
// Plugin instance for the default theme
this.defaultTheme;
+
+ // i18n for themes
+ this.i18n = new I18n();
}
util.inherits(_WebsiteOutput, Output);
@@ -68,15 +72,22 @@ WebsiteOutput.prototype.prepare = function() {
that.themeDefault? that.themeDefault.root : null
])
.compact()
- .map(templatesPath)
.uniq()
.value();
- that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(searchPaths));
+ // Load i18n
+ _.each(searchPaths.reverse(), function(searchPath) {
+ var i18nRoot = path.resolve(searchPath, '_i18n');
+
+ if (!fs.existsSync(i18nRoot)) return;
+ that.i18n.load(i18nRoot);
+ });
+
+
+ that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(_.map(searchPaths, templatesPath)));
that.env.addFilter('t', function(s) {
- // todo: i18n
- return s;
+ return that.i18n.t(that.book.config.get('language'), s);
});
// Transform an absolute path into a relative path
diff --git a/package.json b/package.json
index a5905c48e..474effa48 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,8 @@
"cp": "0.2.0",
"cpr": "1.0.0",
"direction": "0.1.5",
- "moment": "2.11.2"
+ "moment": "2.11.2",
+ "i18n-t": "1.0.0"
},
"devDependencies": {
"eslint": "1.5.0",
From a446deaff32714c81fb02c7138de48fafa8216c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 15:52:04 +0100
Subject: [PATCH 131/217] Complete generator for ebooks
---
lib/output/base.js | 18 +++++-
lib/output/ebook.js | 145 ++++++++++++++++++++++++++++++++++++++++++-
lib/utils/command.js | 22 ++++++-
package.json | 2 +-
4 files changed, 181 insertions(+), 6 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 41905597d..828a11e07 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -7,7 +7,6 @@ var pathUtil = require('../utils/path');
var location = require('../utils/location');
var PluginsManager = require('../plugins');
var TemplateEngine = require('../template');
-var gitbook = require('../gitbook');
/*
Output is like a stream interface for a parsed book
@@ -16,9 +15,13 @@ to output "something".
The process is mostly on the behavior of "onPage" and "onAsset"
*/
-function Output(book) {
+function Output(book, opts) {
_.bindAll(this);
+ this.opts = _.defaults(opts || {}, {
+
+ });
+
this.book = book;
this.log = this.book.log;
@@ -175,6 +178,17 @@ Output.prototype.onResolveTemplate = function(from, to) {
// ---- Utilities ----
+// Return a default context for templates
+Output.prototype.getContext = function() {
+ return _.extend(
+ {},
+ this.book.getContext(),
+ this.book.summary.getContext(),
+ this.book.glossary.getContext(),
+ this.book.config.getContext()
+ );
+};
+
// Resolve a file path in the context of a specific page
// Result is an "absolute path relative to the output folder"
Output.prototype.resolveForPage = function(page, href) {
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index 7e7a66d60..a0e72a9ac 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -1,14 +1,155 @@
+var _ = require('lodash');
var util = require('util');
+var juice = require('juice');
+var command = require('../utils/command');
+var fs = require('../utils/fs');
var WebsiteOutput = require('./website');
var assetsInliner = require('./assets-inliner');
-function EbookOutput() {
+function _EbookOutput() {
WebsiteOutput.apply(this, arguments);
}
util.inherits(EbookOutput, WebsiteOutput);
+var EbookOutput = assetsInliner(_EbookOutput);
+
EbookOutput.prototype.name = 'ebook';
+// Finish generation, create ebook using ebook-convert
+EbookOutput.prototype.finish = function() {
+ var that = this;
-module.exports = assetsInliner(EbookOutput);
+ return Promise()
+ .then(function() {
+ return EbookOutput.super_.prototype.finish.apply(that);
+ })
+
+ // Generate SUMMARY.html
+ .then(function() {
+ return that.render('summary', that.getContext())
+ .then(function(html) {
+ return that.writeFile(
+ that.resolve('SUMMARY.html'),
+ html
+ );
+ });
+ })
+
+ // Start ebook-convert
+ .then(function() {
+ return that.ebookConvertOption();
+ })
+
+ .then(function(options) {
+ var cmd = [
+ 'ebook-convert',
+ that.resolve('SUMMARY.html'),
+ that.resolve('index.'+that.opts.format),
+ command.optionsToShellArgs(options)
+ ].join(' ');
+
+ return command.exec(cmd)
+ .fail(function(err) {
+ if (err.code == 127) {
+ err = new Error('Need to install ebook-convert from Calibre');
+ }
+
+ throw err;
+ });
+ });
+};
+
+// Generate header/footer for PDF
+EbookOutput.prototype.getPDFTemplate = function(tpl) {
+ var that = this;
+ var context = _.extend(
+ {
+ // Nunjucks context mapping to ebook-convert templating
+ page: {
+ num: '_PAGENUM_',
+ title: '_TITLE_'
+ }
+ },
+ this.getContext()
+ );
+
+ return this.render('pdf_'+tpl, context)
+
+ // Inline css, include css relative to the output folder
+ .then(function(output) {
+ return Promise.nfcall(juice.juiceResources, tpl, {
+ webResources: {
+ relativeTo: that.root()
+ }
+ });
+ });
+};
+
+// Generate options for ebook-convert
+EbookOutput.prototype.ebookConvertOption = function() {
+ var that = this;
+ var cover = this.book.config.get('cover');
+
+ if (!cover && fs.existsSync(this.resolve('cover.jpg'))) {
+ cover = this.resolve('cover.jpg');
+ }
+
+ var options = {
+ '--cover': cover,
+ '--title': this.book.config.get('title'),
+ '--comments': this.book.config.get('description'),
+ '--isbn': this.book.config.get('isbn'),
+ '--authors': this.book.config.get('author'),
+ '--language': this.book.config.get('language'),
+ '--book-producer': 'GitBook',
+ '--publisher': 'GitBook',
+ '--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]',
+ '--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]',
+ '--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]',
+ '--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]',
+ '--no-chapters-in-toc': true,
+ '--max-levels': '1',
+ '--breadth-first': true
+ };
+
+ if (this.opts.format == 'epub') {
+ options = _.extend(options, {
+ '--dont-split-on-page-breaks': true
+ });
+ }
+
+ if (this.opts.format != 'pdf') return Promise(options);
+
+ var pdfOptions = this.book.config.get('pdf');
+
+ options = _.extend(options, {
+ '--chapter-mark': String(pdfOptions.chapterMark),
+ '--page-breaks-before': String(pdfOptions.pageBreaksBefore),
+ '--margin-left': String(pdfOptions.margin.left),
+ '--margin-right': String(pdfOptions.margin.right),
+ '--margin-top': String(pdfOptions.margin.top),
+ '--margin-bottom': String(pdfOptions.margin.bottom),
+ '--pdf-default-font-size': String(pdfOptions.fontSize),
+ '--pdf-mono-font-size': String(pdfOptions.fontSize),
+ '--paper-size': String(pdfOptions.paperSize),
+ '--pdf-page-numbers': Boolean(pdfOptions.pageNumbers),
+ '--pdf-header-template': that.getPDFTemplate('header'),
+ '--pdf-footer-template': that.getPDFTemplate('footer'),
+ '--pdf-sans-family': String(pdfOptions.fontFamily)
+ });
+
+ return this.getPDFTemplate('header')
+ .then(function(tpl) {
+ options['--pdf-header-template'] = tpl;
+
+ return that.getPDFTemplate('footer');
+ })
+ .then(function(tpl) {
+ options['--pdf-footer-template'] = tpl;
+
+ return options;
+ });
+};
+
+module.exports = EbookOutput;
diff --git a/lib/utils/command.js b/lib/utils/command.js
index c3e33c7f0..49b09988a 100644
--- a/lib/utils/command.js
+++ b/lib/utils/command.js
@@ -1,3 +1,4 @@
+var _ = require('lodash');
var childProcess = require('child_process');
var Promise = require('./promise');
@@ -37,8 +38,27 @@ function spawn(command, args, options) {
return d.promise;
}
+// Transform an option object to a command line string
+function escapeShellArg(s) {
+ s = s.replace(/"/g, '\\"');
+ return '"' + s + '"';
+}
+
+function optionsToShellArgs(options) {
+ return _.chain(options)
+ .map(function(value, key) {
+ if (value === null || value === undefined || value === false) return null;
+ if (value === true) return key;
+ return key + '=' + escapeShellArg(value);
+ })
+ .compact()
+ .value()
+ .join(' ');
+}
+
module.exports = {
isAvailable: isAvailable,
exec: exec,
- spawn: spawn
+ spawn: spawn,
+ optionsToShellArgs: optionsToShellArgs
};
diff --git a/package.json b/package.json
index 474effa48..2710dde1d 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"dom-serializer": "0.1.0",
"spawn-cmd": "0.0.2",
"escape-string-regexp": "1.0.3",
- "juice": "1.5.0",
+ "juice": "1.9.0",
"jsonschema": "1.0.2",
"json-schema-defaults": "0.1.1",
"merge-defaults": "0.2.1",
From a9c96c1afa8c9322645d25684d4171ba540a4d8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 17:23:07 +0100
Subject: [PATCH 132/217] Fix resolve of relatives images in assets inliner
---
lib/output/assets-inliner.js | 6 +++++-
lib/output/folder.js | 10 ----------
test/assets-inliner.js | 20 ++++++++++++++++++--
3 files changed, 23 insertions(+), 13 deletions(-)
diff --git a/lib/output/assets-inliner.js b/lib/output/assets-inliner.js
index 6768830e0..6f1f02d4a 100644
--- a/lib/output/assets-inliner.js
+++ b/lib/output/assets-inliner.js
@@ -63,12 +63,16 @@ module.exports = function assetsInliner(Base) {
})
.then(function() {
+ // Resolve src to a relative filepath to the book's root
+ src = page.resolveLocal(src);
+
+ // Already a PNG/JPG/.. ?
if (path.extname(src).toLowerCase() != '.svg') {
return src;
}
// Convert SVG to PNG
- return that.convertSVGFile(that.resolveOutputForPage(page, src));
+ return that.convertSVGFile(that.resolve(src));
})
// Return relative path from the page
diff --git a/lib/output/folder.js b/lib/output/folder.js
index 4d8584edc..f5b706332 100644
--- a/lib/output/folder.js
+++ b/lib/output/folder.js
@@ -73,16 +73,6 @@ module.exports = function folderOutput(Base) {
return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
};
- // Resolve a file path from a page (in the output folder)
- // Result is an absolute path (enforced in outptu folder)
- FolderOutput.prototype.resolveOutputForPage = function(page, filename) {
- if (_.isString(page)) page = this.book.getPage(page);
-
- var abs = page.resolveLocal(filename);
- return this.resolve(abs);
- };
-
-
// Copy a file to the output
FolderOutput.prototype.copyFile = function(from, to) {
var that = this;
diff --git a/test/assets-inliner.js b/test/assets-inliner.js
index 498d3cbc4..7ccabc56b 100644
--- a/test/assets-inliner.js
+++ b/test/assets-inliner.js
@@ -13,12 +13,15 @@ describe('Assets Inliner Output', function() {
return mock.outputDefaultBook(AssetsInliner, {
'README.md': '',
- // test for SVGS
+ // SVGs
'svg_file.md': '',
'svg_inline.md': 'This is a svg: '+SVG,
'test.svg': '' + SVG,
- // test for remote files
+ // Relative
+ 'folder/test.md': '',
+
+ // Remote images
'remote_png.md': '',
'remote_svg.md': '',
@@ -26,6 +29,7 @@ describe('Assets Inliner Output', function() {
'* [svg file](svg_file.md)\n' +
'* [remote png file](remote_png.md)\n' +
'* [remote svg file](remote_svg.md)\n' +
+ '* [relative image](folder/test.md)\n' +
'\n\n'
})
.then(function(_output) {
@@ -43,8 +47,14 @@ describe('Assets Inliner Output', function() {
// Does the file exists
var src = $img.attr('src');
+
+ // Resolve the filename
+ src = page.resolveLocal(src);
+
output.should.have.file(src);
path.extname(src).should.equal('.png');
+
+ return src;
}
describe('SVG', function() {
@@ -66,5 +76,11 @@ describe('Assets Inliner Output', function() {
testImageInPage('remote_svg.md');
});
});
+
+ describe('Relative Images', function() {
+ it('should correctly resolve image', function() {
+ testImageInPage('folder/test.md');
+ });
+ });
});
From cef4096b39b9543d0fb2ff9867d86d6e12fdf38e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 17:24:26 +0100
Subject: [PATCH 133/217] Add cli commands to build ebooks
---
lib/cli/index.js | 31 +++++++++++++++++++++++++++++++
lib/output/ebook.js | 3 ++-
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/lib/cli/index.js b/lib/cli/index.js
index 07c4888b0..448ea0198 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -7,11 +7,38 @@ var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
var Book = require('../book');
var NodeFS = require('../fs/node');
+var EBookOutput = require('../output/ebook');
var helper = require('./helper');
var Server = require('./server');
var watch = require('./watch');
+function ebookCommand(format) {
+ return {
+ name: format + ' [book] [output]',
+ description: 'generates ebook '+format,
+ options: [
+ helper.options.log
+ ],
+ exec: helper.bookCmd(function(book, args, kwargs) {
+ var output = new EBookOutput(book, {
+ format: format
+ });
+
+ return output.book.parse()
+ .then(function() {
+ // Set output folder
+ if (args[0]) {
+ output.book.config.set('output', path.resolve(process.cwd(), args[0]));
+ }
+
+ return output.generate();
+ });
+ })
+ };
+}
+
+
module.exports = {
commands: [
@@ -71,6 +98,10 @@ module.exports = {
})
},
+ ebookCommand('pdf'),
+ ebookCommand('epub'),
+ ebookCommand('mobi'),
+
{
name: 'serve [book]',
description: 'Build then serve a book from a directory',
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index a0e72a9ac..794f52179 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -4,13 +4,14 @@ var juice = require('juice');
var command = require('../utils/command');
var fs = require('../utils/fs');
+var Promise = require('../utils/promise');
var WebsiteOutput = require('./website');
var assetsInliner = require('./assets-inliner');
function _EbookOutput() {
WebsiteOutput.apply(this, arguments);
}
-util.inherits(EbookOutput, WebsiteOutput);
+util.inherits(_EbookOutput, WebsiteOutput);
var EbookOutput = assetsInliner(_EbookOutput);
From 360381dd9f90a785e1ac7e8c5649215ba6220e2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 17:48:06 +0100
Subject: [PATCH 134/217] Fix write of SUMMARY.html in ebook format
---
lib/output/ebook.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index 794f52179..ce83cefcf 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -31,7 +31,7 @@ EbookOutput.prototype.finish = function() {
return that.render('summary', that.getContext())
.then(function(html) {
return that.writeFile(
- that.resolve('SUMMARY.html'),
+ 'SUMMARY.html',
html
);
});
From 5f058f2ceb77490cbbeaa56523e128e0a46a0dea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:08:36 +0100
Subject: [PATCH 135/217] Disable directoryIndex for ebook generation
---
lib/output/base.js | 4 ++--
lib/output/ebook.js | 3 +++
lib/output/website.js | 6 ++++--
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index 828a11e07..cc4eb8d42 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -19,7 +19,7 @@ function Output(book, opts) {
_.bindAll(this);
this.opts = _.defaults(opts || {}, {
-
+ directoryIndex: true
});
this.book = book;
@@ -222,7 +222,7 @@ Output.prototype.outputPath = function(filename, ext) {
Output.prototype.outputUrl = function(filename, ext) {
var href = this.outputPath(filename, ext);
- if (path.basename(href) == 'index.html') {
+ if (path.basename(href) == 'index.html' && this.opts.directoryIndex) {
href = path.dirname(href) + '/';
}
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index ce83cefcf..6d531f8a6 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -10,6 +10,9 @@ var assetsInliner = require('./assets-inliner');
function _EbookOutput() {
WebsiteOutput.apply(this, arguments);
+
+ // ebook-convert does not support link like "./"
+ this.opts.directoryIndex = false;
}
util.inherits(_EbookOutput, WebsiteOutput);
diff --git a/lib/output/website.js b/lib/output/website.js
index 32647f6c6..01021e587 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -103,8 +103,10 @@ WebsiteOutput.prototype.prepare = function() {
// Relase path to an asset
that.env.addFilter('resolveAsset', function(href) {
- href = path.join('/gitbook', href);
- return that.resolveForPage(this.ctx.file.path, href);
+ href = path.join('gitbook', href);
+ if (!this.ctx.file) return href;
+
+ return that.resolveForPage(this.ctx.file.path, '/' + href);
});
})
From ab541c165366feeebda10e1b5dfce42ba1b5db3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:23:59 +0100
Subject: [PATCH 136/217] Fix generation of pdf header/footer
---
lib/output/ebook.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index 6d531f8a6..2db5be3fc 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -72,7 +72,8 @@ EbookOutput.prototype.getPDFTemplate = function(tpl) {
// Nunjucks context mapping to ebook-convert templating
page: {
num: '_PAGENUM_',
- title: '_TITLE_'
+ title: '_TITLE_',
+ section: '_SECTION_'
}
},
this.getContext()
@@ -82,7 +83,7 @@ EbookOutput.prototype.getPDFTemplate = function(tpl) {
// Inline css, include css relative to the output folder
.then(function(output) {
- return Promise.nfcall(juice.juiceResources, tpl, {
+ return Promise.nfcall(juice.juiceResources, output, {
webResources: {
relativeTo: that.root()
}
From 7b2e332f6c0f3b3fa3e278bb82585bdd4e2deb0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:33:07 +0100
Subject: [PATCH 137/217] Complete ebook command to output one file
---
lib/cli/helper.js | 37 +++++++++++++++++++++++++++++++++++++
lib/cli/index.js | 33 +++------------------------------
2 files changed, 40 insertions(+), 30 deletions(-)
diff --git a/lib/cli/helper.js b/lib/cli/helper.js
index 6dae58c24..9510b4992 100644
--- a/lib/cli/helper.js
+++ b/lib/cli/helper.js
@@ -4,6 +4,7 @@ var path = require('path');
var Book = require('../book');
var NodeFS = require('../fs/node');
var Logger = require('../utils/logger');
+var fs = require('../utils/fs');
var JSONOutput = require('../output/json');
var WebsiteOutput = require('../output/website');
var EBookOutput = require('../output/ebook');
@@ -57,9 +58,45 @@ function outputCmd(fn) {
});
}
+// Command to generate an ebook
+function ebookCmd(format) {
+ return {
+ name: format + ' [book] [output] [file]',
+ description: 'generates ebook '+format,
+ options: [
+ LOG_OPTION
+ ],
+ exec: bookCmd(function(book, args, kwargs) {
+ return fs.tmpDir()
+ .then(function(dir) {
+ var outputFile = path.resolve(process.cwd(), args[1] || 'book.' + format);
+ var output = new EBookOutput(book, {
+ format: format
+ });
+
+ return output.book.parse()
+ .then(function() {
+ // Set output folder
+ output.book.config.set('output', dir);
+ return output.generate();
+ })
+
+ // Copy the ebook file
+ .then(function() {
+ return fs.copy(
+ path.resolve(dir, 'index.' + format),
+ outputFile
+ );
+ });
+ });
+ })
+ };
+}
+
module.exports = {
bookCmd: bookCmd,
outputCmd: outputCmd,
+ ebookCmd: ebookCmd,
options: {
log: LOG_OPTION,
diff --git a/lib/cli/index.js b/lib/cli/index.js
index 448ea0198..78c528661 100644
--- a/lib/cli/index.js
+++ b/lib/cli/index.js
@@ -7,38 +7,11 @@ var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
var Book = require('../book');
var NodeFS = require('../fs/node');
-var EBookOutput = require('../output/ebook');
var helper = require('./helper');
var Server = require('./server');
var watch = require('./watch');
-function ebookCommand(format) {
- return {
- name: format + ' [book] [output]',
- description: 'generates ebook '+format,
- options: [
- helper.options.log
- ],
- exec: helper.bookCmd(function(book, args, kwargs) {
- var output = new EBookOutput(book, {
- format: format
- });
-
- return output.book.parse()
- .then(function() {
- // Set output folder
- if (args[0]) {
- output.book.config.set('output', path.resolve(process.cwd(), args[0]));
- }
-
- return output.generate();
- });
- })
- };
-}
-
-
module.exports = {
commands: [
@@ -98,9 +71,9 @@ module.exports = {
})
},
- ebookCommand('pdf'),
- ebookCommand('epub'),
- ebookCommand('mobi'),
+ helper.ebookCmd('pdf'),
+ helper.ebookCmd('epub'),
+ helper.ebookCmd('mobi'),
{
name: 'serve [book]',
From 96d5b37196e2536663eaf6e485c897a358ac5259 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:38:11 +0100
Subject: [PATCH 138/217] Update 1.0.0-pre.1 of theme-default
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 2710dde1d..fee58a8c7 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
"gitbook-plugin-fontsettings": "1.0.2",
- "gitbook-plugin-theme-default": "git+https://github.com/GitbookIO/theme-default.git#master",
+ "gitbook-plugin-theme-default": "1.0.0-pre.1",
"nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
From 8a3396deba3de1094117decaf3bda2bba8679f8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:38:27 +0100
Subject: [PATCH 139/217] Add releases notes to documentation
---
docs/SUMMARY.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 247fc27af..12871cff3 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -29,3 +29,4 @@
--
* [FAQ](faq.md)
+* [Release notes](https://github.com/GitbookIO/gitbook/blob/master/CHANGES.md)
From 7f9c24eedbc3967b52254f0d9344ca1fb1a4f9d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:43:37 +0100
Subject: [PATCH 140/217] Fix test for file.mtime
---
test/page.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/test/page.js b/test/page.js
index 8eb209649..37acc959e 100644
--- a/test/page.js
+++ b/test/page.js
@@ -289,7 +289,8 @@ describe('Page', function() {
var page = book.addPage('variables/file/mtime.md');
return page.toHTML(output)
.then(function(content) {
- content.should.endWith('(CET)\n');
+ // A date ends with "(CET)" or "(UTC)""
+ content.should.endWith(')\n');
});
});
From 1e486a16bd9a05db1e9550aad7228765d73e310b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 18:48:50 +0100
Subject: [PATCH 141/217] Update theme-default@1.0.0-pre.2
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index fee58a8c7..66ac058e1 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
"gitbook-plugin-fontsettings": "1.0.2",
- "gitbook-plugin-theme-default": "1.0.0-pre.1",
+ "gitbook-plugin-theme-default": "1.0.0-pre.2",
"nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
From 7de152539d7834df62a9579590ed839ef55d2bb5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 19:08:39 +0100
Subject: [PATCH 142/217] Fix order of search paths for nunjucks
---
docs/book.js | 6 +++---
lib/output/website.js | 7 +++++--
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/docs/book.js b/docs/book.js
index b04e91cda..988abb26c 100644
--- a/docs/book.js
+++ b/docs/book.js
@@ -1,10 +1,10 @@
var pkg = require('../package.json');
module.exports = {
- "title": "GitBook Documentation",
+ title: 'GitBook Documentation',
- plugins: ["theme-official"],
- theme: "official",
+ plugins: ['theme-official'],
+ theme: 'official',
variables: {
version: pkg.version
}
diff --git a/lib/output/website.js b/lib/output/website.js
index 01021e587..d35ece36f 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -60,6 +60,10 @@ WebsiteOutput.prototype.prepare = function() {
throw new Error('Theme "' + themeName + '" is not installed, add "' + themeID(themeName) + '" to your "book.json"');
}
+ if (that.themeDefault.root != that.theme.root) {
+ that.log.info.ln('build using theme "' + themeName + '"');
+ }
+
// This list is ordered to give priority to templates in the book
var searchPaths = _.chain([
// The book itself can contains a "_layouts" folder
@@ -76,14 +80,13 @@ WebsiteOutput.prototype.prepare = function() {
.value();
// Load i18n
- _.each(searchPaths.reverse(), function(searchPath) {
+ _.each(searchPaths.concat().reverse(), function(searchPath) {
var i18nRoot = path.resolve(searchPath, '_i18n');
if (!fs.existsSync(i18nRoot)) return;
that.i18n.load(i18nRoot);
});
-
that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(_.map(searchPaths, templatesPath)));
that.env.addFilter('t', function(s) {
From a186544fadf040bc2b4c83dfe41b6bd2a482c5a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 19:28:20 +0100
Subject: [PATCH 143/217] Add defaults filters to website tpl engine
---
lib/output/website.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/lib/output/website.js b/lib/output/website.js
index d35ece36f..835b8b52b 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -6,6 +6,7 @@ var I18n = require('i18n-t');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
+var defaultFilters = require('../template/filters');
var conrefsLoader = require('./conrefs');
var Output = require('./base');
@@ -89,6 +90,12 @@ WebsiteOutput.prototype.prepare = function() {
that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(_.map(searchPaths, templatesPath)));
+ // Add GitBook default filters
+ _.each(defaultFilters, function(fn, filter) {
+ that.env.addFilter(filter, fn);
+ });
+
+ // Translate using _i18n locales
that.env.addFilter('t', function(s) {
return that.i18n.t(that.book.config.get('language'), s);
});
From d139d1d1bd65e3dd0eff2987f355bb16d8c24058 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Sun, 21 Feb 2016 19:41:38 +0100
Subject: [PATCH 144/217] Add "template.theme" and "template.default" variables
---
docs/_layouts/website/page.html | 14 ++++++++++++++
lib/output/website.js | 14 +++++++++++++-
2 files changed, 27 insertions(+), 1 deletion(-)
create mode 100644 docs/_layouts/website/page.html
diff --git a/docs/_layouts/website/page.html b/docs/_layouts/website/page.html
new file mode 100644
index 000000000..edce25173
--- /dev/null
+++ b/docs/_layouts/website/page.html
@@ -0,0 +1,14 @@
+{% extends template.theme %}
+
+{% block page %}
+{{ super() }}
+
+
+{% endblock %}
diff --git a/lib/output/website.js b/lib/output/website.js
index 835b8b52b..929c82f96 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -179,7 +179,19 @@ WebsiteOutput.prototype.onPage = function(page) {
// Render a template using nunjucks
// Templates are stored in `_layouts` folders
WebsiteOutput.prototype.render = function(tpl, context) {
- return Promise.nfcall(this.env.render.bind(this.env), this.templateName(tpl), context);
+ var filename = this.templateName(tpl);
+
+ context = _.extend(context, {
+ template: {
+ // Same template but in the default theme
+ default: this.themeDefault? path.resolve(templatesPath(this.themeDefault.root), filename) : null,
+
+ // Same template but in the theme
+ theme: path.resolve(templatesPath(this.theme.root), filename)
+ }
+ });
+
+ return Promise.nfcall(this.env.render.bind(this.env), filename, context);
};
// Return a complete name for a template
From 4dbe81cb660aa3422e4e5238459b48e357785c0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 08:54:46 +0100
Subject: [PATCH 145/217] Add base changelog
---
CHANGES.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CHANGES.md b/CHANGES.md
index e1dbf9f94..5706144b7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+## 3.x.x (unreleased)
+- Summary can contain external links and anchors (Fix [#776](https://github.com/GitbookIO/gitbook/issues/776))
+- Summary can contain differents entitled sections
+- Glossary is generated as a normal page
+- Headings are no longer annotated with glossary terms
+- Themes are now published as a plugin, with ability to extend it from the book source
+- `links.sidebar` configuration is no longer supported, use summary sections instead
+- `pdf.headerTemplate` and `pdf.footerTemplate` have been replaced by a template in theme/book: `_layout/ebook/pdf_header.html` and `_layout/ebook/pdf_footer.html`
+
## 2.6.7
- Fix bug with filenames including spaces
- Add Turkish and Catalan translations
From ccc585ca1fd47ea6cb24cf16b82fd58304840d9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 08:59:19 +0100
Subject: [PATCH 146/217] Add doc for plugins
---
docs/plugins.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/docs/plugins.md b/docs/plugins.md
index e69de29bb..4d9e7b62d 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -0,0 +1,22 @@
+# Plugins
+
+Plugins are the best way to extend GitBook functionalities (ebook and website). There exist plugins to do a lot of things: bring math formulas display support, track visits using Google Analytic, etc.
+
+### How to find plugins?
+
+Plugins can be easily searched on [plugins.gitbook.com](https://plugins.gitbook.com).
+
+
+### How to install a plugin?
+
+Once you find a plugin that you want to install, you need to add it to your book.json:
+
+```
+{
+ "plugins": ["myPlugin", "anotherPlugin"]
+}
+```
+
+You can also specify a specific version using: "myPlugin@0.3.1". By default GitBook will resolve the latest version of the plugin compatbile with the current GitBook version.
+
+Plugins are automatically installed on [GitBook.com](https://www.gitbook.com). Locally, run `gitbook install` to install and prepare all plugins for your books.
From de31b900803892ef0a441ccffba5d5f6a6c42105 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 10:11:44 +0100
Subject: [PATCH 147/217] Add validation of plugin's config after loading
---
lib/plugins/index.js | 5 ++
lib/plugins/plugin.js | 114 +++++++++++++++++++++++++++++++++++++++---
lib/utils/error.js | 7 +++
test/plugins.js | 32 ++++++++----
4 files changed, 143 insertions(+), 15 deletions(-)
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 343c569a2..00ee7d103 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -104,4 +104,9 @@ PluginsManager.prototype.hook = function(name, input) {
}, input);
};
+// Extract all resources for a namespace
+PluginsManager.prototype.resources = function(namespace) {
+
+};
+
module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 9f9cc35de..df7918480 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -1,6 +1,9 @@
var _ = require('lodash');
var path = require('path');
var resolve = require('resolve');
+var mergeDefaults = require('merge-defaults');
+var jsonschema = require('jsonschema');
+var jsonSchemaDefaults = require('json-schema-defaults');
var Promise = require('../utils/promise');
var error = require('../utils/error');
@@ -11,6 +14,8 @@ var HOOKS = [
'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
];
+var RESOURCES = ['js', 'css'];
+
// Return true if an error is a "module not found"
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
function isModuleNotFound(err) {
@@ -37,18 +42,23 @@ BookPlugin.prototype.isLoaded = function() {
};
// Load this plugin
-BookPlugin.prototype.load = function() {
+// An optional folder to search in can be passed
+BookPlugin.prototype.load = function(folder) {
var that = this;
if (this.isLoaded()) {
return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
}
- // Try loading plugins from different location
- var p = Promise.some([
+ // Fodlers to search plugins in
+ var searchPaths = _.compact([
+ folder,
this.book.resolve('node_modules'),
__dirname
- ], function(baseDir) {
+ ]);
+
+ // Try loading plugins from different location
+ var p = Promise.some(searchPaths, function(baseDir) {
// Locate plugin and load pacjage.json
try {
var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });
@@ -82,13 +92,21 @@ BookPlugin.prototype.load = function() {
return true;
})
- .then(that.validate);
+ .then(that.validate)
+
+ // Validate the configuration and update it
+ .then(function() {
+ var config = that.book.config.get(that.getConfigKey(), {});
+ return that.validateConfig(config);
+ })
+ .then(function(config) {
+ that.book.config.set(that.getConfigKey(), config);
+ });
this.log.info('Loading plugin "' + this.id + '" ...');
return this.log.info.promise(p);
};
-
// Verify the definition of a plugin
// Also verify that the plugin accepts the current gitbook version
// This method throws erros if plugin is invalid
@@ -113,6 +131,42 @@ BookPlugin.prototype.validate = function() {
}
};
+// Normalize, validate configuration for this plugin using its schema
+// Throw an error when shcema is not respected
+BookPlugin.prototype.validateConfig = function(config) {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ var schema = that.packageInfos.gitbook || {};
+ if (!schema) return config;
+
+ // Normalize schema
+ schema.id = '/'+that.getConfigKey();
+ schema.type = 'object';
+
+ // Validate and throw if invalid
+ var v = new jsonschema.Validator();
+ var result = v.validate(config, schema, {
+ propertyName: that.getConfigKey()
+ });
+
+ // Throw error
+ if (result.errors.length > 0) {
+ throw new error.ConfigurationError(new Error(result.errors[0].stack));
+ }
+
+ // Insert default values
+ var defaults = jsonSchemaDefaults(schema);
+ return mergeDefaults(config, defaults);
+ });
+};
+
+// Return key for configuration
+BookPlugin.prototype.getConfigKey = function() {
+ return 'pluginsConfig.'+this.id;
+};
+
// Call a hook and returns its result
BookPlugin.prototype.hook = function(name, input) {
// Our book will be the context to apply
@@ -134,4 +188,52 @@ BookPlugin.prototype.hook = function(name, input) {
});
};
+// Return resources without normalization
+BookPlugin.prototype._getResources = function(base) {
+ base = base;
+ var book = this.infos[base];
+
+ // Compatibility with version 1.x.x
+ if (base == 'website') book = book || this.infos.book;
+
+ // Nothing specified, fallback to default
+ if (!book) {
+ return Promise({});
+ }
+
+ // Dynamic function
+ if(typeof book === 'function') {
+ // Call giving it the context of our book
+ return Promise().then(book.bind(this.book));
+ }
+
+ // Plain data object
+ return Promise(_.cloneDeep(book));
+};
+
+// Normalize resources and return them
+BookPlugin.prototype.getResources = function(base) {
+ var that = this;
+
+ return this._getResources(base)
+ .then(function(resources) {
+
+ _.each(RESOURCES, function(resourceType) {
+ resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
+ });
+
+ return resources;
+ });
+};
+
+// Normalize filters and return them
+BookPlugin.prototype.getFilters = function() {
+ return this.content.filters || {};
+};
+
+// Normalize blocks and return them
+BookPlugin.prototype.getBlocks = function() {
+ return this.content.blocks || {};
+};
+
module.exports = BookPlugin;
diff --git a/lib/utils/error.js b/lib/utils/error.js
index 53d822144..52281bfc1 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -55,6 +55,12 @@ var PluginError = WrappedError({
plugin: null
});
+// Error for nunjucks templates
+var ConfigurationError = WrappedError({
+ message: 'Error with book\'s configuration: {origMessage}',
+ type: 'configuration'
+});
+
// Deprecate methods/fields
function deprecateMethod(fn, msg) {
return deprecated.method(msg, log.warn.ln, fn);
@@ -74,6 +80,7 @@ module.exports = {
TemplateError: TemplateError,
PluginError: PluginError,
+ ConfigurationError: ConfigurationError,
deprecateMethod: deprecateMethod,
deprecateField: deprecateField
diff --git a/test/plugins.js b/test/plugins.js
index 47f93143c..a525a7bfa 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -1,7 +1,13 @@
+var path = require('path');
+
var mock = require('./mock');
var registry = require('../lib/plugins/registry');
var Output = require('../lib/output/base');
+var error = require('../lib/utils/error');
var PluginsManager = require('../lib/plugins');
+var BookPlugin = require('../lib/plugins/plugin');
+
+var PLUGINS_ROOT = path.resolve(__dirname, 'node_modules');
describe('Plugins', function() {
var book;
@@ -20,15 +26,6 @@ describe('Plugins', function() {
});
});
- describe('Loading', function() {
- it('should load default plugins', function() {
- return mock.outputDefaultBook(Output)
- .then(function(output) {
- output.plugins.count().should.be.greaterThan(0);
- });
- });
- });
-
describe('Installation', function() {
it('should install a plugin from NPM without a specific version', function() {
return registry.install(book, 'ga')
@@ -65,5 +62,22 @@ describe('Plugins', function() {
.should.be.fulfilledWith(1);
});
});
+
+ describe('Loading', function() {
+ it('should load default plugins', function() {
+ return mock.outputDefaultBook(Output)
+ .then(function(output) {
+ output.plugins.count().should.be.greaterThan(0);
+ });
+ });
+ });
+
+ describe('Configuration', function() {
+ it('should fail loading a plugin with an invalid configuration', function() {
+ var plugin = new BookPlugin(book, 'test-config');
+ return plugin.load(PLUGINS_ROOT)
+ .should.be.rejectedWith('Error with book\'s configuration: pluginsConfig.test-config.myProperty is required');
+ });
+ });
});
From b23d3a9f37a225eb418967a19c4ddddbf8a76c63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 10:16:38 +0100
Subject: [PATCH 148/217] Add test for extension of book's configuration
---
test/plugins.js | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/test/plugins.js b/test/plugins.js
index a525a7bfa..4fd59d420 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -3,7 +3,6 @@ var path = require('path');
var mock = require('./mock');
var registry = require('../lib/plugins/registry');
var Output = require('../lib/output/base');
-var error = require('../lib/utils/error');
var PluginsManager = require('../lib/plugins');
var BookPlugin = require('../lib/plugins/plugin');
@@ -78,6 +77,28 @@ describe('Plugins', function() {
return plugin.load(PLUGINS_ROOT)
.should.be.rejectedWith('Error with book\'s configuration: pluginsConfig.test-config.myProperty is required');
});
+
+ it('should extend configuration with default properties', function() {
+ return mock.setupBook({
+ 'book.json': {
+ pluginsConfig: {
+ 'test-config': {
+ 'myProperty': 'world'
+ }
+ }
+ }
+ })
+ .then(function(book2) {
+ return book2.config.load()
+ .then(function() {
+ var plugin = new BookPlugin(book2, 'test-config');
+ return plugin.load(PLUGINS_ROOT);
+ })
+ .then(function() {
+ book2.config.get('pluginsConfig.test-config.myDefaultProperty', '').should.equal('hello');
+ });
+ });
+ });
});
});
From de234944090dcb2356db1185e3fba7935dc6ee11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 10:20:43 +0100
Subject: [PATCH 149/217] Change gitignore to commit tests plugins
---
.gitignore | 2 +-
.../gitbook-plugin-test-config/index.js | 1 +
.../gitbook-plugin-test-config/package.json | 21 +++++++++++++++++++
3 files changed, 23 insertions(+), 1 deletion(-)
create mode 100644 test/node_modules/gitbook-plugin-test-config/index.js
create mode 100644 test/node_modules/gitbook-plugin-test-config/package.json
diff --git a/.gitignore b/.gitignore
index a7a8f664e..259b0a6e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,7 +22,7 @@ build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
-node_modules
+/node_modules
# vim swapfile
*.swp
diff --git a/test/node_modules/gitbook-plugin-test-config/index.js b/test/node_modules/gitbook-plugin-test-config/index.js
new file mode 100644
index 000000000..f053ebf79
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-config/index.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/test/node_modules/gitbook-plugin-test-config/package.json b/test/node_modules/gitbook-plugin-test-config/package.json
new file mode 100644
index 000000000..74bb95357
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-config/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "gitbook-plugin-test-config",
+ "version": "1.0.0",
+ "engines": {
+ "gitbook": "*"
+ },
+ "gitbook": {
+ "properties": {
+ "myProperty": {
+ "type": "string",
+ "required": true,
+ "title": "This is a required property"
+ },
+ "myDefaultProperty": {
+ "type": "string",
+ "default": "hello",
+ "title": "This is a required property"
+ }
+ }
+ }
+}
\ No newline at end of file
From 1e243281c7f0444162dfb8593e9fd9ac2aa725ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 10:36:28 +0100
Subject: [PATCH 150/217] Add test for resources listing
---
lib/plugins/plugin.js | 26 ++++++++++++++++---
.../gitbook-plugin-test-resources/index.js | 12 +++++++++
.../package.json | 7 +++++
test/plugins.js | 24 +++++++++++++++++
4 files changed, 66 insertions(+), 3 deletions(-)
create mode 100644 test/node_modules/gitbook-plugin-test-resources/index.js
create mode 100644 test/node_modules/gitbook-plugin-test-resources/package.json
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index df7918480..0d69cd93c 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -1,5 +1,6 @@
var _ = require('lodash');
var path = require('path');
+var url = require('url');
var resolve = require('resolve');
var mergeDefaults = require('merge-defaults');
var jsonschema = require('jsonschema');
@@ -191,10 +192,10 @@ BookPlugin.prototype.hook = function(name, input) {
// Return resources without normalization
BookPlugin.prototype._getResources = function(base) {
base = base;
- var book = this.infos[base];
+ var book = this.content[base];
// Compatibility with version 1.x.x
- if (base == 'website') book = book || this.infos.book;
+ if (base == 'website') book = book || this.content.book;
// Nothing specified, fallback to default
if (!book) {
@@ -211,6 +212,25 @@ BookPlugin.prototype._getResources = function(base) {
return Promise(_.cloneDeep(book));
};
+// Normalize a specific resource
+BookPlugin.prototype.normalizeResource = function(resource) {
+ // Parse the resource path
+ var parsed = url.parse(resource);
+
+ // This is a remote resource
+ // so we will simply link to using it's URL
+ if (parsed.protocol) {
+ return {
+ 'url': resource
+ };
+ }
+
+ // This will be copied over from disk
+ // and shipped with the book's build
+ return { 'path': this.npmId+'/'+resource };
+};
+
+
// Normalize resources and return them
BookPlugin.prototype.getResources = function(base) {
var that = this;
@@ -219,7 +239,7 @@ BookPlugin.prototype.getResources = function(base) {
.then(function(resources) {
_.each(RESOURCES, function(resourceType) {
- resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
+ resources[resourceType] = _.map(resources[resourceType] || [], that.normalizeResource);
});
return resources;
diff --git a/test/node_modules/gitbook-plugin-test-resources/index.js b/test/node_modules/gitbook-plugin-test-resources/index.js
new file mode 100644
index 000000000..e95e411aa
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-resources/index.js
@@ -0,0 +1,12 @@
+module.exports = {
+ book: {
+ assets: './assets',
+ js: [
+ 'myfile.js',
+ 'https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js'
+ ],
+ css: [
+ 'myfile.css'
+ ]
+ }
+};
diff --git a/test/node_modules/gitbook-plugin-test-resources/package.json b/test/node_modules/gitbook-plugin-test-resources/package.json
new file mode 100644
index 000000000..606de3114
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-resources/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "gitbook-plugin-test-resources",
+ "version": "1.0.0",
+ "engines": {
+ "gitbook": "*"
+ }
+}
\ No newline at end of file
diff --git a/test/plugins.js b/test/plugins.js
index 4fd59d420..576c96b0b 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -100,5 +100,29 @@ describe('Plugins', function() {
});
});
});
+
+ describe('Resources', function() {
+ var plugin;
+
+ before(function() {
+ plugin = new BookPlugin(book, 'test-resources');
+ return plugin.load(PLUGINS_ROOT);
+ });
+
+
+ it('should list all resources for website', function() {
+ return plugin.getResources('website')
+ .then(function(resources) {
+ resources.assets.should.equal('./assets');
+
+ resources.js.should.have.lengthOf(2);
+ resources.js[0].path.should.equal('gitbook-plugin-test-resources/myfile.js');
+ resources.js[1].url.should.equal('https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js');
+
+ resources.css.should.have.lengthOf(1);
+ resources.css[0].path.should.equal('gitbook-plugin-test-resources/myfile.css');
+ });
+ });
+ });
});
From d8fd6430ed5e15d0dee33f730fb6e8e8346c866a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 11:15:34 +0100
Subject: [PATCH 151/217] Copy plugins resources to website output
---
lib/output/website.js | 15 +++++++++++++++
lib/plugins/index.js | 17 +++++++++++++++++
lib/plugins/plugin.js | 1 -
3 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/lib/output/website.js b/lib/output/website.js
index 929c82f96..30e0e8c33 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -174,6 +174,21 @@ WebsiteOutput.prototype.onPage = function(page) {
});
};
+// Finish generation, create ebook using ebook-convert
+WebsiteOutput.prototype.finish = function() {
+ var that = this;
+
+ return Promise()
+ .then(function() {
+ return WebsiteOutput.super_.prototype.finish.apply(that);
+ })
+
+ // Copy assets from plugins
+ .then(function() {
+ return that.plugins.copyResources('website', that.resolve('gitbook'));
+ });
+};
+
// ----- Utilities ----
// Render a template using nunjucks
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 00ee7d103..ed3aa0ac4 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -1,6 +1,8 @@
var _ = require('lodash');
+var path = require('path');
var Promise = require('../utils/promise');
+var fs = require('../utils/fs');
var BookPlugin = require('./plugin');
var registry = require('./registry');
@@ -109,4 +111,19 @@ PluginsManager.prototype.resources = function(namespace) {
};
+// Copy all resources for a plugin
+PluginsManager.prototype.copyResources = function(namespace, outputRoot) {
+ return Promise.serie(this.plugins, function(plugin) {
+ return plugin.getResources(namespace)
+ .then(function(resources) {
+ if (!resources.assets) return;
+
+ var input = path.resolve(plugin.root, resources.assets);
+ var output = path.resolve(outputRoot, plugin.npmId);
+
+ return fs.copyDir(input, output);
+ });
+ });
+};
+
module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 0d69cd93c..094c82dbc 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -237,7 +237,6 @@ BookPlugin.prototype.getResources = function(base) {
return this._getResources(base)
.then(function(resources) {
-
_.each(RESOURCES, function(resourceType) {
resources[resourceType] = _.map(resources[resourceType] || [], that.normalizeResource);
});
From 395bd62663614c79181e3e27049345231186339b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 12:31:02 +0100
Subject: [PATCH 152/217] Provide "plugins" context to theme templates
---
lib/output/website.js | 19 +++++++++++++++--
lib/plugins/index.js | 11 +++++++++-
lib/plugins/plugin.js | 47 +++++++++++++++++++++++++++++--------------
3 files changed, 59 insertions(+), 18 deletions(-)
diff --git a/lib/output/website.js b/lib/output/website.js
index 30e0e8c33..c1b0a6c91 100644
--- a/lib/output/website.js
+++ b/lib/output/website.js
@@ -32,6 +32,9 @@ function _WebsiteOutput() {
// Plugin instance for the default theme
this.defaultTheme;
+ // Resources loaded from plugins
+ this.resources;
+
// i18n for themes
this.i18n = new I18n();
}
@@ -120,7 +123,7 @@ WebsiteOutput.prototype.prepare = function() {
});
})
- // Copy assets before copyign files from book
+ // Copy assets from themes before copying files from book
.then(function() {
return Promise.serie([
// Assets from the book are already copied
@@ -150,6 +153,14 @@ WebsiteOutput.prototype.prepare = function() {
}
);
});
+ })
+
+ // Load resources for plugins
+ .then(function() {
+ return that.plugins.getResources(that.name)
+ .then(function(resources) {
+ that.resources = resources;
+ });
});
};
@@ -185,7 +196,7 @@ WebsiteOutput.prototype.finish = function() {
// Copy assets from plugins
.then(function() {
- return that.plugins.copyResources('website', that.resolve('gitbook'));
+ return that.plugins.copyResources(that.name, that.resolve('gitbook'));
});
};
@@ -203,6 +214,10 @@ WebsiteOutput.prototype.render = function(tpl, context) {
// Same template but in the theme
theme: path.resolve(templatesPath(this.theme.root), filename)
+ },
+
+ plugins: {
+ resources: this.resources
}
});
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index ed3aa0ac4..bed4488a1 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -107,8 +107,17 @@ PluginsManager.prototype.hook = function(name, input) {
};
// Extract all resources for a namespace
-PluginsManager.prototype.resources = function(namespace) {
+PluginsManager.prototype.getResources = function(namespace) {
+ return Promise.reduce(this.plugins, function(out, plugin) {
+ return plugin.getResources(namespace)
+ .then(function(pluginResources) {
+ _.each(BookPlugin.RESOURCES, function(resourceType) {
+ out[resourceType] = (out[resourceType] || []).concat(pluginResources[resourceType] || []);
+ });
+ return out;
+ });
+ }, {});
};
// Copy all resources for a plugin
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 094c82dbc..7fb44b241 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -34,6 +34,9 @@ function BookPlugin(book, pluginId) {
this.packageInfos = undefined;
this.content = undefined;
+ // Cache for resources
+ this._resources = {};
+
_.bindAll(this);
}
@@ -191,25 +194,37 @@ BookPlugin.prototype.hook = function(name, input) {
// Return resources without normalization
BookPlugin.prototype._getResources = function(base) {
- base = base;
- var book = this.content[base];
+ var that = this;
- // Compatibility with version 1.x.x
- if (base == 'website') book = book || this.content.book;
+ return Promise()
+ .then(function() {
+ if (that._resources[base]) return that._resources[base];
- // Nothing specified, fallback to default
- if (!book) {
- return Promise({});
- }
+ base = base;
+ var book = that.content[base];
- // Dynamic function
- if(typeof book === 'function') {
- // Call giving it the context of our book
- return Promise().then(book.bind(this.book));
- }
+ // Compatibility with version 1.x.x
+ if (base == 'website') book = book || that.content.book;
- // Plain data object
- return Promise(_.cloneDeep(book));
+ // Nothing specified, fallback to default
+ if (!book) {
+ return Promise({});
+ }
+
+ // Dynamic function
+ if(typeof book === 'function') {
+ // Call giving it the context of our book
+ return book.call(that.book);
+ }
+
+ // Plain data object
+ return book;
+ })
+
+ .then(function(resources) {
+ that._resources[base] = resources;
+ return _.cloneDeep(resources);
+ });
};
// Normalize a specific resource
@@ -256,3 +271,5 @@ BookPlugin.prototype.getBlocks = function() {
};
module.exports = BookPlugin;
+module.exports.RESOURCES = RESOURCES;
+
From 71144db09c150f6499d977863dbccf12ce05638b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 13:54:52 +0100
Subject: [PATCH 153/217] Normalize context for plugins
---
lib/plugins/context.js | 24 ++++++++++++++++++++++++
lib/plugins/plugin.js | 16 ++++++++++------
lib/utils/logger.js | 7 ++++++-
3 files changed, 40 insertions(+), 7 deletions(-)
create mode 100644 lib/plugins/context.js
diff --git a/lib/plugins/context.js b/lib/plugins/context.js
new file mode 100644
index 000000000..51649aa22
--- /dev/null
+++ b/lib/plugins/context.js
@@ -0,0 +1,24 @@
+var error = require('../utils/error');
+
+/*
+ Return the context for a plugin.
+ It tries to keep compatibilities with GitBook v2
+*/
+
+function pluginCtx(plugin) {
+ var book = plugin.book;
+ var ctx = {
+ config: book.config,
+ log: plugin.log,
+
+ // Paths
+ resolve: book.resolve
+ };
+
+ // Deprecation
+ error.deprecateField(ctx, 'options', book.config.dump(), '"options" property is deprecated, use config.get(key) instead');
+
+ return ctx;
+}
+
+module.exports = pluginCtx;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 7fb44b241..c26288705 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -10,6 +10,7 @@ var Promise = require('../utils/promise');
var error = require('../utils/error');
var gitbook = require('../gitbook');
var registry = require('./registry');
+var pluginCtx = require('./context');
var HOOKS = [
'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
@@ -25,7 +26,7 @@ function isModuleNotFound(err) {
function BookPlugin(book, pluginId) {
this.book = book;
- this.log = this.book.log;
+ this.log = this.book.log.prefix(pluginId);
this.id = pluginId;
this.npmId = registry.npmId(pluginId);
@@ -45,6 +46,11 @@ BookPlugin.prototype.isLoaded = function() {
return Boolean(this.packageInfos && this.content);
};
+// Bind a function to the plugin's context
+BookPlugin.prototype.bind = function(fn) {
+ return fn.bind(pluginCtx(this));
+};
+
// Load this plugin
// An optional folder to search in can be passed
BookPlugin.prototype.load = function(folder) {
@@ -173,9 +179,7 @@ BookPlugin.prototype.getConfigKey = function() {
// Call a hook and returns its result
BookPlugin.prototype.hook = function(name, input) {
- // Our book will be the context to apply
- var context = this.book;
-
+ var that = this;
var hookFunc = this.content.hooks? this.content.hooks[name] : null;
input = input || {};
@@ -188,7 +192,7 @@ BookPlugin.prototype.hook = function(name, input) {
return Promise()
.then(function() {
- return hookFunc.apply(context, [input]);
+ return that.bind(hookFunc)(input);
});
};
@@ -214,7 +218,7 @@ BookPlugin.prototype._getResources = function(base) {
// Dynamic function
if(typeof book === 'function') {
// Call giving it the context of our book
- return book.call(that.book);
+ return that.bind(book)();
}
// Plain data object
diff --git a/lib/utils/logger.js b/lib/utils/logger.js
index 9dda57d48..60215af4c 100644
--- a/lib/utils/logger.js
+++ b/lib/utils/logger.js
@@ -17,7 +17,7 @@ var COLORS = {
ERROR: color.red
};
-function Logger(write, logLevel) {
+function Logger(write, logLevel, prefix) {
if (!(this instanceof Logger)) return new Logger(write, logLevel);
this._write = write || function(msg) { process.stdout.write(msg); };
@@ -40,6 +40,11 @@ function Logger(write, logLevel) {
}, this);
}
+// Create a new logger prefixed from this logger
+Logger.prototype.prefix = function(prefix) {
+ return (new Logger(this._write, this.logLevel, prefix));
+};
+
// Change minimum level
Logger.prototype.setLevel = function(logLevel) {
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
From 642ba72a1d0680107357083a79560c2ccc931457 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 14:50:55 +0100
Subject: [PATCH 154/217] Load all filters and blocks from plugins in output's
template
---
lib/output/base.js | 6 +++++-
lib/plugins/context.js | 3 +++
lib/plugins/index.js | 18 +++++++++++++++++-
lib/plugins/plugin.js | 31 +++++++++++++++++++++++++++++--
lib/template/index.js | 6 ++----
5 files changed, 56 insertions(+), 8 deletions(-)
diff --git a/lib/output/base.js b/lib/output/base.js
index cc4eb8d42..fdddfe80b 100644
--- a/lib/output/base.js
+++ b/lib/output/base.js
@@ -47,7 +47,11 @@ Output.prototype.generate = function() {
// Load all plugins
.then(function() {
- return that.plugins.loadAll();
+ return that.plugins.loadAll()
+ .then(function() {
+ that.template.addFilters(that.plugins.getFilters());
+ that.template.addBlocks(that.plugins.getBlocks());
+ });
})
// Initialize the generation
diff --git a/lib/plugins/context.js b/lib/plugins/context.js
index 51649aa22..a192b7bc4 100644
--- a/lib/plugins/context.js
+++ b/lib/plugins/context.js
@@ -18,6 +18,9 @@ function pluginCtx(plugin) {
// Deprecation
error.deprecateField(ctx, 'options', book.config.dump(), '"options" property is deprecated, use config.get(key) instead');
+ // Loop for template filters/blocks
+ error.deprecateField(ctx, 'book', ctx, '"book" property is deprecated, this directly instead');
+
return ctx;
}
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index bed4488a1..2ebbcc8c4 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -69,7 +69,7 @@ PluginsManager.prototype.load = function(name) {
PluginsManager.prototype.loadAll = function() {
var plugins = _.pluck(this.book.config.get('plugins'), 'name');
- this.log.info.ln('installing', plugins.length, 'plugins');
+ this.log.info.ln('loading', plugins.length, 'plugins');
return this.load(plugins);
};
@@ -135,4 +135,20 @@ PluginsManager.prototype.copyResources = function(namespace, outputRoot) {
});
};
+// Get all filters and blocks
+PluginsManager.prototype.getFilters = function() {
+ return _.reduce(this.plugins, function(out, plugin) {
+ var filters = plugin.getFilters();
+
+ return _.extend(out, filters);
+ }, {});
+};
+PluginsManager.prototype.getBlocks = function() {
+ return _.reduce(this.plugins, function(out, plugin) {
+ var blocks = plugin.getBlocks();
+
+ return _.extend(out, blocks);
+ }, {});
+};
+
module.exports = PluginsManager;
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index c26288705..0fbbab1ab 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -266,12 +266,39 @@ BookPlugin.prototype.getResources = function(base) {
// Normalize filters and return them
BookPlugin.prototype.getFilters = function() {
- return this.content.filters || {};
+ var that = this;
+
+ return _.mapValues(this.content.filters || {}, function(fn, filter) {
+ return function() {
+ var ctx = _.extend(
+ this,
+ pluginCtx(that)
+ );
+
+ return fn.apply(ctx, arguments);
+ };
+ });
};
// Normalize blocks and return them
BookPlugin.prototype.getBlocks = function() {
- return this.content.blocks || {};
+ var that = this;
+
+ return _.mapValues(this.content.blocks || {}, function(block, blockName) {
+ block = _.isFunction(block)? { exec: block } : block;
+
+ var fn = block.exec;
+ block.exec = function() {
+ var ctx = _.extend(
+ this,
+ pluginCtx(that)
+ );
+
+ return fn.apply(ctx, arguments);
+ };
+
+ return block;
+ });
};
module.exports = BookPlugin;
diff --git a/lib/template/index.js b/lib/template/index.js
index a130088a1..8a523929c 100644
--- a/lib/template/index.js
+++ b/lib/template/index.js
@@ -70,12 +70,10 @@ function TemplateEngine(output) {
TemplateEngine.prototype.bindContext = function(func) {
var ctx = {
ctx: this.ctx,
- book: this.book,
- output: this.output
+ output: this.output,
+ generator: this.output.name
};
- error.deprecateField(ctx, 'generator', this.output.name, 'generator property is deprecated, use output.name instead');
-
return _.bind(func, ctx);
};
From 6653307fda137253cd10d4d4f6d2e010c1c0f604 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 15:34:35 +0100
Subject: [PATCH 155/217] Add tests for context of filters
---
lib/plugins/context.js | 2 +-
lib/plugins/plugin.js | 10 ++-----
.../gitbook-plugin-test-filters/index.js | 21 +++++++++++++++
.../gitbook-plugin-test-filters/package.json | 7 +++++
test/plugins.js | 27 +++++++++++++++++++
5 files changed, 58 insertions(+), 9 deletions(-)
create mode 100644 test/node_modules/gitbook-plugin-test-filters/index.js
create mode 100644 test/node_modules/gitbook-plugin-test-filters/package.json
diff --git a/lib/plugins/context.js b/lib/plugins/context.js
index a192b7bc4..17cefb6d9 100644
--- a/lib/plugins/context.js
+++ b/lib/plugins/context.js
@@ -19,7 +19,7 @@ function pluginCtx(plugin) {
error.deprecateField(ctx, 'options', book.config.dump(), '"options" property is deprecated, use config.get(key) instead');
// Loop for template filters/blocks
- error.deprecateField(ctx, 'book', ctx, '"book" property is deprecated, this directly instead');
+ error.deprecateField(ctx, 'book', ctx, '"book" property is deprecated, use "this" directly instead');
return ctx;
}
diff --git a/lib/plugins/plugin.js b/lib/plugins/plugin.js
index 0fbbab1ab..490157987 100644
--- a/lib/plugins/plugin.js
+++ b/lib/plugins/plugin.js
@@ -270,10 +270,7 @@ BookPlugin.prototype.getFilters = function() {
return _.mapValues(this.content.filters || {}, function(fn, filter) {
return function() {
- var ctx = _.extend(
- this,
- pluginCtx(that)
- );
+ var ctx = _.extend(pluginCtx(that), this);
return fn.apply(ctx, arguments);
};
@@ -289,10 +286,7 @@ BookPlugin.prototype.getBlocks = function() {
var fn = block.exec;
block.exec = function() {
- var ctx = _.extend(
- this,
- pluginCtx(that)
- );
+ var ctx = _.extend(pluginCtx(that), this);
return fn.apply(ctx, arguments);
};
diff --git a/test/node_modules/gitbook-plugin-test-filters/index.js b/test/node_modules/gitbook-plugin-test-filters/index.js
new file mode 100644
index 000000000..71f27520b
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-filters/index.js
@@ -0,0 +1,21 @@
+var should = require('should');
+
+module.exports = {
+ filters: {
+ // Simple test
+ hello: function(s) {
+ return 'Hello ' + s + '!';
+ },
+
+ // Test using the conetxt "this"
+ testContext: function(s) {
+ this.should.have.property('config');
+ this.should.have.property('log');
+ this.should.have.property('options');
+ this.should.have.property('resolve').which.is.a.function;
+ this.should.have.property('book').which.equal(this);
+
+ return 'Hello ' + s + '!';
+ }
+ }
+};
diff --git a/test/node_modules/gitbook-plugin-test-filters/package.json b/test/node_modules/gitbook-plugin-test-filters/package.json
new file mode 100644
index 000000000..d555d0693
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-filters/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "gitbook-plugin-test-filters",
+ "version": "1.0.0",
+ "engines": {
+ "gitbook": "*"
+ }
+}
\ No newline at end of file
diff --git a/test/plugins.js b/test/plugins.js
index 576c96b0b..03bbc266b 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -1,3 +1,4 @@
+var _ = require('lodash');
var path = require('path');
var mock = require('./mock');
@@ -124,5 +125,31 @@ describe('Plugins', function() {
});
});
});
+
+ describe('Filters', function() {
+ var plugin, filters;
+
+ before(function() {
+ plugin = new BookPlugin(book, 'test-filters');
+ return plugin.load(PLUGINS_ROOT)
+
+ .then(function() {
+ filters = plugin.getFilters();
+ });
+ });
+
+
+ it('should list all resources for website', function() {
+ _.size(filters).should.equal(2);
+ });
+
+ it('should correctly execute a filter', function() {
+ filters.hello('World').should.equal('Hello World!');
+ });
+
+ it('should correctly set contexts for filter', function() {
+ filters.testContext('Hello');
+ });
+ });
});
From 0b3bdcb2193e10f5bf0f6959475563d8c430aa40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 15:39:36 +0100
Subject: [PATCH 156/217] Add tests for blocks from plugins
---
.../gitbook-plugin-test-blocks/index.js | 21 ++++++++++++++
.../gitbook-plugin-test-blocks/package.json | 7 +++++
test/plugins.js | 28 +++++++++++++++++--
3 files changed, 54 insertions(+), 2 deletions(-)
create mode 100644 test/node_modules/gitbook-plugin-test-blocks/index.js
create mode 100644 test/node_modules/gitbook-plugin-test-blocks/package.json
diff --git a/test/node_modules/gitbook-plugin-test-blocks/index.js b/test/node_modules/gitbook-plugin-test-blocks/index.js
new file mode 100644
index 000000000..710400602
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-blocks/index.js
@@ -0,0 +1,21 @@
+var should = require('should');
+
+module.exports = {
+ blocks: {
+ // Simple test
+ hello: function(blk) {
+ return 'Hello ' + blk.body + '!';
+ },
+
+ // Test using the conetxt "this"
+ testContext: function(s) {
+ this.should.have.property('config');
+ this.should.have.property('log');
+ this.should.have.property('options');
+ this.should.have.property('resolve').which.is.a.function;
+ this.should.have.property('book').which.equal(this);
+
+ return 'Hello ' + s + '!';
+ }
+ }
+};
diff --git a/test/node_modules/gitbook-plugin-test-blocks/package.json b/test/node_modules/gitbook-plugin-test-blocks/package.json
new file mode 100644
index 000000000..a756fb515
--- /dev/null
+++ b/test/node_modules/gitbook-plugin-test-blocks/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "gitbook-plugin-test-blocks",
+ "version": "1.0.0",
+ "engines": {
+ "gitbook": "*"
+ }
+}
\ No newline at end of file
diff --git a/test/plugins.js b/test/plugins.js
index 03bbc266b..8207663ba 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -138,8 +138,7 @@ describe('Plugins', function() {
});
});
-
- it('should list all resources for website', function() {
+ it('should list all filters', function() {
_.size(filters).should.equal(2);
});
@@ -151,5 +150,30 @@ describe('Plugins', function() {
filters.testContext('Hello');
});
});
+
+ describe('Blocks', function() {
+ var plugin, blocks;
+
+ before(function() {
+ plugin = new BookPlugin(book, 'test-blocks');
+ return plugin.load(PLUGINS_ROOT)
+
+ .then(function() {
+ blocks = plugin.getBlocks();
+ });
+ });
+
+ it('should list all blocks', function() {
+ _.size(blocks).should.equal(2);
+ });
+
+ it('should correctly normalize block', function() {
+ blocks.hello.exec({ body: 'World' }).should.equal('Hello World!');
+ });
+
+ it('should correctly set contexts for filter', function() {
+ blocks.testContext.exec({ body: 'Hello' });
+ });
+ });
});
From c7b1d72bf0bec4758958040f375632305bbbffbe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 15:46:53 +0100
Subject: [PATCH 157/217] Add basic test for ebook generation
---
lib/output/ebook.js | 2 ++
test/all.js | 1 +
test/output-ebook.js | 33 +++++++++++++++++++++++++++++++++
test/output-website.js | 4 ++++
4 files changed, 40 insertions(+)
create mode 100644 test/output-ebook.js
diff --git a/lib/output/ebook.js b/lib/output/ebook.js
index 2db5be3fc..fc9cfd707 100644
--- a/lib/output/ebook.js
+++ b/lib/output/ebook.js
@@ -46,6 +46,8 @@ EbookOutput.prototype.finish = function() {
})
.then(function(options) {
+ if (!that.opts.format) return;
+
var cmd = [
'ebook-convert',
that.resolve('SUMMARY.html'),
diff --git a/test/all.js b/test/all.js
index d8f227561..5a96e385d 100644
--- a/test/all.js
+++ b/test/all.js
@@ -23,3 +23,4 @@ require('./page');
require('./assets-inliner');
require('./output-json');
require('./output-website');
+require('./output-ebook');
diff --git a/test/output-ebook.js b/test/output-ebook.js
new file mode 100644
index 000000000..c43fc5508
--- /dev/null
+++ b/test/output-ebook.js
@@ -0,0 +1,33 @@
+var mock = require('./mock');
+var EbookOutput = require('../lib/output/ebook');
+
+describe('Ebook Output', function() {
+ describe('Sample Book', function() {
+ var output;
+
+ before(function() {
+ return mock.outputDefaultBook(EbookOutput)
+ .then(function(_output) {
+ output = _output;
+ });
+ });
+
+ it('should correctly generate an index.html', function() {
+ output.should.have.file('index.html');
+ });
+
+ it('should correctly generate a SUMMARY.html', function() {
+ output.should.have.file('index.html');
+ });
+
+ it('should correctly copy assets', function() {
+ output.should.have.file('gitbook/ebook.css');
+ });
+
+ it('should correctly copy plugins', function() {
+ output.should.have.file('gitbook/gitbook-plugin-highlight/ebook.css');
+ });
+
+ });
+
+});
diff --git a/test/output-website.js b/test/output-website.js
index 9fe1928a1..69adf8860 100644
--- a/test/output-website.js
+++ b/test/output-website.js
@@ -22,6 +22,10 @@ describe('Website Output', function() {
output.should.have.file('gitbook/images/favicon.ico');
});
+ it('should correctly copy plugins', function() {
+ output.should.have.file('gitbook/gitbook-plugin-highlight/website.css');
+ });
+
});
describe('Book with chapters', function() {
From 1bf6abf3385799aa8df7058139fcb8420943dee8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Samy=20Pess=C3=A9?=
Date: Mon, 22 Feb 2016 16:58:47 +0100
Subject: [PATCH 158/217] Pass options to context of theme
---
book.pdf | Bin 0 -> 173360 bytes
lib/output/website.js | 5 +++--
2 files changed, 3 insertions(+), 2 deletions(-)
create mode 100644 book.pdf
diff --git a/book.pdf b/book.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..2ec0c28e9d1acdc749b412d2cf1d470d31f9ee97
GIT binary patch
literal 173360
zcma&O1CVW7(luJPZC9PDQ?_l}wr$(CZQHhO`;<=Eey8u>{k^{5>v;V~>^)<}j2$Ca
zWR5x4$eFn|shp4qH9ZXz6zO2~VAVhg5fmwbpo6i#ld%zjfjfbrzLmLwgE0XE4HG>L
z9f78qlan0>E$u(2sf`V6Z7pdGZLPJTpy(M0=m=~LETFi!2xx_*2pH&D85jv@Ma-?7
zj2#FxX+^C5fC?EK+8P;a6VOT-+n73;5iqbZ(-ZLUKsh@7v1twEmNl)l8++6O>(kpi
zFbim7px*0Wz~?ZAdMasu3FHm8r4_M9Beod&^^PhmPsCi9U2n>ru8EVv6pBLOlfpZh
zqg?8qV_mnH<+C$eieYGH?=XoH#Sx4!*yQ6~O7`;ldE)x*{!H+^1(%;h&G2kzN|Mpqxbh-#z*oT-L
zn^h3viyoA8V?bt%dJKGkj^s$RzC$9cPL0--2s%sTdA-z>_JQ4@NTZ|WH;Q$
zEm4#_;s-wx&Q2atBte2jgh_Q@$GmW2TZShUpTt#g%Lo+*Mb0cP=TK&V$U`lya3`7`
zTQQF!OnF!q&Opu+Q3{oFHM}QayyOJlW
zW)cZSz-Sg)03ux1OAoD%hZ3-eC~|@jKST}EOMoRb`^#m24B|$VfC|hNw8b8jUkQs*
z(h`F#6C)USWe<@c2}luW(TzI>VeBF#9KjKeLr;qRxnCB$SS0gedXJzYc}WslL?Ez9
zE~{uEhM3hrSN&DMD$FO7_USUZzrqD4%gj(8`}IkyQ~xkI85fu@iWK{8Q
z4i^jJ`?Gx;si
zTP0WGA&8QQb3_RHT(~_Mvo(1uOxrS*3pHNxMI;^Y=^=ql}&>9&(Q$Nw;88
z`=xJ5A`oUM)f}_NaS~RZ@^5jp?-5CqLj6}5+4_~_|3tC=uPFZi`ug9a_@8|(|L;-!
z%h$Hi2yK;$&Pe`OA5LgMBam2Uy507*~!tenC
zGKWaKBm*|ZeGMxd0SQB~nz&^K)S!a&-*&Ko+W*%t*o6Fa%1ZEjVak6v^Aap{P^xtx
z#cYmBNFW*Hr%D415`*whuX6Sf0{|{+&f|~9a1mwE?;$Dda6kuy+qAR_1
zCoX$
zEKT*5_RTOY{^sibV36kR(xNDV8`K%1FX8&;rBVkT?+_c$$g1LGuMG%A51Ft}j=f0K
z3D8)xceJ&?Iel9@C@3+y42+zV)rP0G5hveK&+2YvbJKIPvKP6ep4rya@-c8Sv!A)5
zp4rvZ;_~2E7ueF&YHMu!2-=@JXxiw`8}f`~6s0BC@N|dcwrQSv?-)fpS=u?c(x$#o
zIjSE9?QCu9RFRDCG=Hl*Kw9^*{pF@>9~)@D!^Y9HC}QbO;ihMQbqYGB*4x_VIXmyw
z>0@QjKYp=w$dP)@1W!FP<6>)H?U;L~3ffZK@<~I7o_M`Ui&E&+VXaD7B*>^QPmGq#
z&VHfx3v9DML0qI!A{y80i+DPU3x_5&{c$*U(-;BwHJEqUPBINNRphuHUv*HEneB=e
zp0mQj6327)5pt)9MPK12H$C(D({ZTPl6;t*-Mz_!kormu8n-)3ktCEh`a{WP7N>>
zH~~&cGXygbONB2RkEY)vjqYwpTaDXky)Fl2%oisBw%Z+=c4z|d8GsO0>5bUv0r#jT
zOlQPSuwYI2Hd@HGJnJFE+u9otvbD3vHd)kld^)>dV(6KX2o%PDt-?3y*F#W;CqNGmce1LmNjPuf*6|kH=YaA6=RGj!tv6
zpRDPb!`Crc|9Et4Oujjlwj9uGK7UN~@~zC1J!h}+xpd#c&dwfW-)RMQx<|`sNwt*~
zg}ydJTz7?cc|UMweC$fpKa0ai@rH|99xdS5akFL3y6VT`?TCVVaumH7LF^vejZjnI
z{kqX9l3G*T3eHj5a>Lvf>t)FieID30d->Im&od10{ABgwMQKPEUMOtgq}p=n;rzVS
z@v3gKvQcLl5P@mNo)MUgvdLQu=UU&8XS#@sh$=jmzvY*rTsQ{r9!d39W7TWH$MyVb_Z?v{6NHS1@)tLS}?eakx3
z3jg?tfe`9jgB*=|qVFOL==8#}^#p6Tz(
zn~9y3<-ZAUR(iI->u$A8nxPMx>fiyj_x@C4tBx=${coj6fRY28c=EKKPt7
z#qC*EM8^5p)eM~WGf|_aBRQnyNe%r_$u+4GBN%~^1LlZnMiBI2w4=b`UfetQKDaiE
zsYCer!|xB?2!h-EsKd9)vz5Z6AfW4=>c^
zHkOnY^Aam^wcL
zCRYnQ5KxL5WlqdcxqHhWZHiiYR6H?(v3D<;sp)jEaK)Yvo#*c{z)xR0PNSKCy^Kw^
z=Y2BOUG8sK09ci)SD6M*0PG<4{Oag|ahUL`pduLTLU(2c1Iahpu1WpPkfsOX)=k?o
zTz9Y#f&`}3Qs#2B28~^ZoR>||-IKI&G<;a%6&>QJrYkJV8>3}x-OPxN+eF9&o)sjM
zZy#zl%-x`T7%szmAQRK&;G%5X49HwwyI#=tfpOS&1$0?!fsd0wz!AsVGx5OoX&r0Fe-I@8fFksS!9f}wbgaDIOWC|M`-7)$K#BVq#
zmFtL0j-0R-yY`#&a<%N~pHVfctu>KiYD36gV9TD?*Vjn{j(luTBLnKHnDYFjF!OIU
zY1_tN7ZN%;(MdK?$Y2Gsr4BdS=L5PumPv*uB_B{9uPrxlw;jTy)st;V8?{QLKz%jG
zV;!xqx-c&-mRU=jKbnn_V-;BpsbUhV=!D;(|J8wCxD~xk%FCDS_ly7ObR{lb=
zK34~ri6`4?%_9wrCi8=0!+Awzxa0(jH44F%8Z%RevPRm$ZB9VEM^Rpl=lOEV7QbVL
zH}Xa{^sesEUG7R9AzQlwkiYd3VSO1^=38Nr`f*BRbFIB~*4G<{Z*=n{#XR`V7tnx?
zBIrLY5%#|?5e8OPrvJJ|7#Nu7{SYUdtYvfv>-K`wlIy*u9Dud=_9f9sO1&wgDqdOl9-UbDYH%=-B?>iTwE==L;lZbR+9
zFtB8$cqj6*ytVpx_J2K`zWJ8O?0((kxq=oxAkgz=%y3X{N(~}UrPm`f_n?lMCu5hh
zF@-$CJz$W6MI>E8zPY&vR9%P!Ol1-1h|S%+7uCDM|6a~vEMFfKB@-dU)Wz$8v)*Y4
z-Cc*zqI@Uze3=Tm+{Gr?h(=f@uhww%%XIN;l|D(inbT!mQDYCR
z+D@38TvewVu%U{E4D;xol|}PIgr6(ZQ$Uh}l0vJ4$)DHG!5VsfM~se~2RP{0D#n~|
zicXLjdmJY^--WY#deYw_)e)3vl5A3+$;TeeA#_~C(b!)ARkZg7f$xfY1(lq;c<17(
z;tRk_fDZvx3L&+lk6kz)R_dY(!)p62^f3bB
zR>gww8OEunbPBE_;6BfZWe~^)0TR+-XQoDi@v5Kr=zl2R
zD%lkqFCQJR>*lp
zVH+i99b0QI+Uj%mIcT#3tb32b6Erocq|R
z;0D)BMKdJWlvf_;P+>uq*`=T@sC<036ek2kEUcWt2NFi+=;(7ojo>tHb`D6hz_W|5
zP!6yf=Y^fItJtO;|L#mNn*JLuC5Ly<(4}rT38piXz$p+34x`8g~A>9-W0X~uP-iX6*5DiLW&>afJN*tGWb
zy8s{7Z1@Wyd2kX=zruHDQD-Nf+Vzfv$hXs0!iPiJRsf(L;dy|j|V2!Vj9%;f0u
zz4Vy0xr}0x&Idx12+QRe7j^^86oFR9HqzsH~soNWDD6G%6
zYDmmqvkYpmC3rL;-0qp|%Lx>+xlx(`3P1~O#&$*241B0AW||
zzPGaCGb;%`kDLfiu?gahd~O!_T5iHK@c3Nm^}5k^JLD)_hWOR^<<-aFbW2U|WY~CR
zFhmqW6FbJOHv4wrXZ&nHT42xRlfN!WT{0Jpq<#S%3rlVStfJKuuEMf3->ICw$5~gT
z9V8EsAP1)ESA^31J)D5q+yF;UY&(1#WZW_y)Yo3olmuQgWf|}!=dge$F}xvoroa21
z8vJzH@3oQ(ul8r2YXvVNR_#T>^FfdMq82y>YC}-^@$2W3OoR#M7V(JOTmaEHD6?RA
zH)#&eG2WbNd6%;*k@v*$N%R`j7*BGP#xh{UIdkfpq6F9web>@c+4dcRBMC{B4dWk})@4`cPPM0{fL72N961{b~T6rfHI
zOeB^Sr|ai$=Up+6mr
z9fdO}czezSXb{B5(Q7S|#(EHQRqa3C+3mqE*8Ojqdr!RaOU%Xmxo;qT;lYC!Gi
zY6*@hykr9f->$>&1)Q+vbQkvccaYlvE(!p734Qyz5PbLAo-?fmOS$ew%}uoNLAMOR
z-(c4$?3e&@F)1~;YfbP!WrnSyl~tlaOVdV0uV5@QzpXw6Y9(AlD7WK@Ql1G}f3f>0
z9DrM28QZu}Vz)0gy(28Wdo~&3yDJo@bGu1Vajz&`r=o_I#^plC1hJmv+Io{(-rFe=
z5sCrh)CN+9L#%z_=3UOHpftUD-~3c9Wm);PRN97Uh;cM&SN!_w;stkJA+Xq|Rb`($Tlmqf`!)-TtWCA?imdor(=(tp{wMwv9`Ew*2~vXHmmE
zzN3aTPZi(eOknl0z04+4J9mQ@2b_jYcs+>1IH`{#PrTM8^(3$^lx}8?NUas$#+l3C
z2)4J?8_~3`s!%~SY$(acoz9^RK|PSYnC4#h1Sc_@l?YDO%${dQPMLy9yc@r61y{e!
zG8@B|$LTvYexx3Cz`tsm$@wyG@_qxl_Xz*|53-1X{_nDgo{5!}?!ToD^vq0u&mAr`
zC2g@;5qd6EZ?yVgcH#5L^(bBZbs|uJJZLh=_~g*7J;rw7+i5RpP7+gM%G%0FAqqr}
z>~ykh$F`HTAoK%`iMnk;V(G!q(-o;O#s$OtKfl@%sd{;Pc0U_E6L}K64I)01c<|RM
zbHBf&>z=<|U_joFN438^j5rUCEk4#+1LWH&Vxm7*Yj^8=kvcyNe?Q#D3VAf@r%1))
z28*I+-3bvxQl&W;GL+a`!wEQ^z5k0Et$lFTobENn1P08bt~F&4Wo
zTuZI0P@k5&I>4-R&Zl;tJl#%Yh%s@5l9doHNFn3WWrGSWNO3rLmC%mHf}xyfVQG1|
z7tBk$8`U(TM6ySdjjIcl48&V>5tJ1&*w64eX-OK;XXKojNhnYSY_S)`wG_qF6oP0X
zPtHF`jhYb+7mTr!Nt#2SV{|9!f!1S4&pK&Q6se)G*S0Eb1aQ9aCR9s$X|JpF)E#%Cwc{k^d)fZRw_HDG+=`4MPmX6e=gv(%=Gf=67e#xT$
zbAF(Ogu^%oyPupX=vB_<_po&Ojl)Md9KjS)e3~;?&r;J#o=l~{pRu+pM%fNCuGM{~
zATyyWbgKi7t)}q?n{4M&A~Xk|jg)dhZh{BTp)x-TKzll}@Sv}6U6SdU!NZpS`y1=V
zHTjOozCkrQ`}ZMpc9nmkGJq5CDc%jb^M|~gTHojknhadbYsne;c_jyC6%K5cUlv$4
zWB?Fo>JG2r2_@kMhc2Y=G80^u?G*qn+ak~xT9p@Yz+^yPT;Dj-7_l<+iR7R{^upY$
zg?yOW#_L>q@&o$I%TG*_%lRS*>O!&2N)bfucx|eP3QEo+=_Cia2muO&y+mQui%Z8C
zkwUD)baj=g=)hy%Skp3JWUj^<`kZ!mQqS_J`vB9~pMg%;J0)(vK9vhkL1$~CO;x$7
zRM?A72Q8{qpjoS|_uKBZNou^RA{{k_pIg1F+M1CkV_fvQc*dwP9(EKjfDMLPU3Ju)
zYnKOwI(Ux+WjTY4#&-bDytkXipu)c8xZ4_mSX$BLP_SUyJLMppg3uoP=oOqQ3GN=7
z-a;eA9y+Fu4GmBi?wUNba=Mwl34h~}-`X*7%}nHF9jTNEF{Jh{B6^GXqS)(wqOW@U
zg$TB5s2N2RDr5P3q3k)nE;ez{L3iJrR3?_i61iS_Gl*}wjjRkB!V-}h1Oa@4RPM)36h-h;appjOo
zg8LwPAJG{Nag@lMoLvc|_~@LM2coN_$ZaJ_q#@F?B==j>PsYibe-uK~B;PvE5$6py
zo5>0o*Qz-&>e@vPmaUhBU12sKdQhPF(|D)Fbm51d3Y84-LV2eg!G{!3^3DM!j!K
z*xulM1(dC+<=oG+=~IQRc1W`=K0H6udS+je>
zeAOK)Ad$7pkZrLJk_)pqeUW;fqIkM=V3Fy;*KTr*BC*I|s)!U@M*W`2%A8T+irsTD^ov<|iCO+xXvMkx2h*gC)>X+G@Q@7v4vIH7aIXBthE9{^tes>s0ZaRdY
z>9JPwr?~}`K*E-sv!#-tN?Zjkx*qQ|8vSE3FR$uv$B27^twso^LzqrT3oe=@>_`fF
z{Gge6e@_cmXU%7#=FEVCm(Z{dbs(li^hy=aEmq&BLq@%R4bK1ch++KuBZiTV{r{pY
z%tFWbzcO#l*yBHC;auJP699XsRuB+C6V`M9cM1iVxRbnTFo*`>OrAff_ucW@=z(eE
z$y$1PlxsZ(?Pvmd1QD+SdZmcOSpYJ_=cfV-{m5S7jRr_%27&>?_sd6~(AWEDE&R9p
z3;n0!fv-R${zC8WkgD(d^Q^+h+indI#n;!T;Mc7>*P2Rk=8R4_dKI-#bIrDwF?UpeD;e%vHa}MPbTGzQ^n%kU&%8=w88WyE5YSnC9>Kt
zLxvM>;;QGQ6%QHZa|_4FpC%<$4MxT3I)h2g5lK!Zz~ZWOs7%vJbIMZenBpZ2pBd%y
z-=yW=j46S#z8VMlmJStLVaAy<2}e@}r#}p3o2yC63{4NZfYz5akRlhHBooQSTOb@*
z2?RndZ6wdxoKOrU4b(Gi%ooW16g#t=8{(|Rahp>OstC}E&r(X-1sjCp?_sEnHRVRz
zCFoTU{(^WVSSK3GM*P`EU0&E;fT4*r`48ZFlJ$bI^`n$6w554A#VFLF0;cVSBU-T*
za)&1l(pFIC2U!$D(0X#|Rwj4R1nPjT*ENHboNtPr6oq%&p?h0_=ID9;vkwtb*N)Q75zg9eAyuaBk(eD8dbP!w
zQ+&=2-JUOq3V6RO4v*jMS<8m+&(el98P3#^+0i=@*Eyc?CHkvkMJv%n-XL91))&Uu
z+?6J;9ao(=DN>~;=wR2r3$h_5d5ImZ&GI`i<0D^!>v1x{S3y_%1ep-|y!
zfM9s(93UkGUc5jYX$@tW_~#wrBf;xhpD+T4(L>QgfE~ahp_L}G=?Bs@rjRX??*iGdesU2(X
zVa}q#7-P>ouAFmd?)xuN+)f>2=>gqNf}kPsV#sqM_c$LHh!i$Zt!-$W^-GKzb*j){
zqZ@gGC_jHc&M}io7&s^`f}?B9fgdXNd{dY0GQP?yYpb?ccQ{Zg2;Qr+FPr1bx9rRZ
zs1G=XS#;
zUW@PiIq(w8<;ba)iKM~h?;)VQ+jJkTI5YcR;u%g0JWkyd@Nt-pgt_G~e-CYvZ^#rB
zKH)*OSeHmFc}0l{qyNDlBSmUhrC9>4I3C>Wjx`gZQ?jx!>;*yt-2Dx5njgV@k+FkR
z^#Ij@k?nbOhz4|09OMGks<#B=waqC858w!j37Cf-U=&0Y=PdH*w}d^1&SqziNp!>D
z7sqY01;5nG!mO6P;up?hqNhvtb53fTQUfR=5aYxIuDyLeI*Gqn8^)w5GREnQgGw%{
zMG`0)6z0@Ko{%%RAT+|9R+~Gg*2NkCvWSP-wJBuVlF~)O6#%_U5v|BW9>jPmO`V*y
z|NT9vo2x4zs4T{BK*VD|cH+#4+$yOPZ^(n33$+u6lg2Q_xa-_6YU76Z5%Xq%s{j&$>|@fXXLf8a3TEaXgN~+=-b0!hJ8>W*1KU3a
zxA3vPiLUO4zOlZM3w|RZu+)M;;-8325ike>-+e--H4(;AHqlV@RW{NznbaN*@-G)CAKy%XIaBH}Q4Jft`nPCCO@9*y6x0
zDX^;r2}n1?-Xn%jDR`6TPWEK&ve+oP^pNJLY@C+aytFXI24Zl%_Bi>4GJ6ISm*$0&
z^=l->aty<+1HzvXKD6(MVuC%uBd=>#i>0zVb=>&+)LZIi{L?M}nE&brbKLXmMtgUa
zlfZZlYxawY@P35}_0V|-J^<@`yHn+|{RtpPstrVOnbVmGz4yhc(Yywo!?v09>#R2P
zll9^?Q)zL_YS|Aty=KJl-oOxUkRGaS~Iahu$>IcZwsAz~1vmg&((
zVELA>u(h{$tWbXXY
zNIv)yjCNC4!U53Bl^^?08*>hJWp^52ZzFRyvQ?OdrYza)V^$_krdpxQ?Iw#8N32=$
z%pv#DWL#D}@a+IVqk6ijTZWz_Pscd{+4{R@C{zh#&k*xHrRJ=v(%y@?Y(&9)k{#o$
z@8a2bSq#=Tjppx!op|am8*F43uXL{LlTA~e{3ie@)f0>n7nuO|tE@>dF64l9{8KbG
z7%T6!C44k#?hhAx(Yc)4!H%`OLXmo6VO!8_Z8ph0onh^98arbE6TR)lmmbV|SNGLD
zzNWOFwYWrABEP!Pn&&~evbK|3n6bB1c5fJNdKsG_ZuQI%2b;4n=MEH5aQ*$_*?hXX%>aYzD!62HrpAzG3zx5=c)Zr3KwGEgV5&QAXj;nQgP%J}VqC59owmjt
z=oz_|=^O@4dkN@PAVr@pU>A?rxb|;ElG`@wm2wN)AQ67!Nv0J6bG)&w0zSn@Z#wK4
za}(?xtoXX6Ai`2GuvcZ)S_gdIC*bo>yh%tkDkoc
zQqPCb?`UEsJ69E2KZHc#NmC(mq+EJd-83N-aE-HPq-BZGB!Sx0&!*N&HG)
z19=)zGEs4$Rg8L@?FKpM63JTJd|y88x_hgE?W65XH68^}FWpFtM7t_G^0vscr@to1
zin2G(s@hDe9gn^*xIT)pD%>pTRmYKpEubj`KjKc##zo8W-E_F$^qD{69TI%DF1!=W
z{A7(eIzCx8WvRaGX)Iq^uDU%m1xbZpt?FNGkt?5S6@ztfcbsh8i$PJdeFV2j6TQwi
zH8@!LBu>M9?1W8?bv+h+pMdyZJ=!FuV<&5uvc{2e1NsP`;>txbz(R}r7>UIF~
zpA;zb-xVkmJNRosb66?viH`mi
z6^YmDwj#YW@Kr`?_cC?cLcg$Gwx$?pVxz6VhIp%^)O~j5H7Q1ZejWA$Vxf3fI0N&X
zchPdB$X)Og*nZii0~xS-NR{Q*m@=7(QMYjny(@XZf&QK}t}*(9#*~tPC*4rP6#nJh
zK{@Id--YneqH}KEjTt0J@WP-V5%*DoDfBkcYPMAJt3y8V?hN#>S?9jXu4<)auNy@kJG%*QTp5SN}{jj=mV
z()_~GWre9ACiO6}Bi~PAF52?}HA~}S4Gd8N{PFT>nIN*CdgS0Bsu%j+IWT4XT%A3W
z_n61~(EQp~V$N4-so|I9s5TTaS*h1vBDi=!Q{PW94My@}sqDaOFu)N=dWsqtdCBOV
zM-rJ3=Q8Xd!+V5_{O110&B>r!Y%YuFs=+&}q}&dP7ZcR)W1q@a{y2U>4B*RbW3YP1
zI<8yz&uhqmY8OT?RS5PRnli!W5{8S9dSpsJ;exrMf?RiE9_{Nc2S%$9LQDFMcbZCF
z0LDkjKP?Mm=`J`W&cY#WxXZwmZIqX-d++*U?6@U-usOSZAV|FK<85TzZPZNe=RcK9mcJ8PThbPX
z6{hE1^;C->VrKzB7?86W`az+HZq9Lz1u&q)Y~c^1Px803anCd(ITv^gM(McbXUxbG
z3V0MyR2>L|U{k`sPNMg;S3y_lOV#KSLgzrA-WQ{d24`1gS#@o8o_iBMM^WC}zdxsT
zzV2qh`*}V&T?p1{dADJFbJn778+u?(=45_I$0gcGJ2|
z&Jm5G?X>SD&1yY>K$~obWPJThI1J1zThockwLX(qiA6DIM0mLU05PYfi6k)e3L%ge
zn|pCw>b%BV-wf5^3*q)La#+|nn>}1~qvrDb2=t~6a^S5Sot5&aQI4}{Gqh^8uwdO%
zFa2Gk9pJ*-;<*D__*t_ZbmlYtsKeQepFir-+NQn9^?KS|LY%<NHuvz@)SFQ
zYJBDbdp!b{ocr_azV+2*2;Cn1RA+%7&51N5()HD6&JUqC<xD5yrK@4v^8xe*Cni&ZS-Q0@?*?7TIQ2m~W7z74FR>E}2Ksp*Y
zxj6wL8%;q-zYhZbToij3IV$zT3|H$NAmcq>BTunQ+X@vU+C$)*XyU$4`PO8-So^H?
zJh5n>HEBzo#X-fsx^zN{8ov$lGUMw-&g*BL|#T;tz)*JYcK6d+pFO}nao2gl161w6#qAy!hFUEadlDkT9c$+id8uyG(
zylWdj3EOB{D;lNB`=U%8u;mLy7DYO1)lwPXKcU@fgY|7bO$XUN%zD?z*QfEL0~)kk
ziHOUKxHtToJ^>$)`#$lSLJRWEF57=AUm&2*4U|V8b;A|Orc`2P?D9n)$7)Xe6!8pQ
zbAf4xOQru+5s)iT-5Gg1h;ehNUXW$1@Z4Jv>@&enrgIw)7$5!Yt`41tEss-fqg7Ia
zwn%H$qYd;{4CI7#R__O2hA$)wk~sXqe;E7$_`q4u>Mhg43W!Tvy(H<-rQyNZ2G({pumSmfI(WYiF7*YIr(f%d6M_$g1j%^8viCx2fL!cERUZ;
zf`_j_;i7Bd9xv3EJ77!KU_rRMfKC)3`HUt5vZ^YIjU)_kDB_b`E64&hz}HRE5RWs}
z#lniPEz87jz4sb-hA2O3hp8WmXd-~U8F@B{%eBf+Z>S{bRwMWfH9NU)vD9#POT-5A
zf>Ua1&s&EZ1>OH$_B3n*Zc#goKL$?XTEH3nFipe?-NqRPUx9|-*G7;nM#Hy@k|(No
z9P@y%9s3fyrE+9b@E*Sz@nRMBOw3NyPSA>XW=i_Wf49I_i?UY=^dZ03jWq`Ss7pQN
z_Zzz|lx|C)8~^5{V-OGAi?{GtWGNG6OvSH9VF6}}hKT6*FLqjIKPt>xVo`=``08yP
z%fQA^kY8oL29Lo^!7=Tho(w+hLK~wm(^5JJXIN&Y?Vl)PxVGTz0q&6JKFzWPeV`Ap
z3lcG8Lcib#@P)R3U&D{YhEl=C(C2~e@epeXaH2j0hh#~19rx?3tvc&`*>sL}3tkn*
zzy06DkKvo~nQA(r2gOyKehi35BP;euw+X;?B028vc}gm-5>5y!fSL;vNNH2NT3Hiu
zGh0~yDAvToB|6_?w5?8qBN|SsOB|kO$)$
z2mpr=D^;bioi@<+{2SPwp=Bq*BJ(@u7aoKDkRppxjl3(}d=a-|i$kNLo|u_*R
z`-d}U0_|`sk?@D4QbA=7GQKDMm7L%oy>*Y~8V3O?IeV%UD}FD~bHf@8G(Hl=>BVN{
zLU=e4!+?4slnU~bnd;h4g(@QSSp*R4g!W6qUczxEskUxf2>B*vGK?2E_a(`RCMEUL
zqS_F8MJJ0tc|DspM(8&HxMAp2Y(Mgam>~M*Mr_h+*Madkb28cbK7sv2Lq0``t!Q;f
zGEYVj8fW!Mkk&CaXao<_G^W+BJM1^$+n23Ny3udt&Jh=N?!B7>cfcmF2(CJK6S;3c
z-~tX0^DBtS(i04j-*smi5*mKU&{XK4rK69$@u
zK|q#4M0>at-^%AofO3{~MpfdbNLZOG*TS)jnQd8Wo8KX_8yuKAgw>@0lvN&{Nw_Z^
zJPa@87TQp2r}PU3_f6QFaxGG~>i~0(lP2AzIVO|8biYwomHMzUP&X+Z5D1yMj9-HD$Fh@TF72#2C^6XvA%j`|(;|oFWyidEb+z-|jxTPQ7>zfKjwBaPoyU_3N4s+bBgcib
z?BOD^5-TBvzbcHiPMS4A(#W&NyEMvl*P4;OfEL9U4R!`SIW2-uPuW6y)vIOEDYF98
zj$4kHyycDPS4>_b)s&Gbv8-|ZiuT5{{1n%ddq4JV)oM+sZ+c|C7XMpum5WY+-fMEz
zNYw5%1-$3W7VNjzFN0hrw@orMyE2!X60fD@WVjg8S&7~j2IZ;6qx}@xMrG~Oi8t;)
zJ*69!1M%Mn`)7mIIpF&=0`t*~zopl3o4Tk+syah!J}easi~Eh5^F!*&-jS9T?~1RM
zrmo$5?|ISHe2)H;c>7;{X!LZ9?5zK-;L6U%@OSN|v1_}?2jg?CyKh=#;@Y!|Bw$cS
zcpJc%>?YDjCJoc?wzqpilj>xiNIPvkuA&!R(SbgC()h{F^P>St0K-;n$dI}MA)wWt
zDsi>pj>qlIt2-enCUbImcKQ7!nYp#7*(JOR(+7ER^QF5W(Uq46vg1RU5%TTEBze8Y
z9F-sObZ@OyYOTiEYnF|FdfbZ}gZ)5!8yN3$T!uR)UFDrD+kC
zFsg0yMcm_SOv$b98zhmrhy7HB1ipoGF-&Y(`yIBGD
zs^p?TJ_Ms{44=W6c&U0i<9a%PBf9?fMzXLYx+&|hXR!}LPeM=rPatn=T(sHNzxXvp
zq{G7WyGbmHA{3o;i8SPN=CS8_>SXG$cDaoBhTtp1lqk&D2^;Tm%_4dgbuD
zt>RT(ff*99b@N)mjJISXTE${8muv)aI0P{l*mX8B#XL;ym^f{_z1Dh*O}YY4y|Pl)
zWi?<9!Z{SimEl~?fe$4O5@#vhmR0EvV+wvTyZUSmK)jLoAtdJTyi^OBHy+Oaj0O=0
zk0d;M7n?FJ6`vp9`}V}P*?Pk>YDKw15afqr7QTZNw>OZ9_^0cs62NzHwLX+O48#wV
zkp1q_f5D*Tm$n5u3#cg3W`y92^{~C9Nr=pMOt4l5YX1OlGf|EPBK&5Ao_UIm6r&?+
z&G*}iK>1g(8+BA?${MGFdd9gL=%Se{(ZslD&b`ae=6eiw
zz!L#;u*GKBsf?}IUO@!37omNTeF)ppa*aTL1{Kg7su~d!51)vGcz;|u)X}~fsMA8I
zW2zf2CofH6xB|Z7S%7&f%QxDhwCz@e5FOEZj$U?pxXg)?SZo3pxZirv@^8ndsqzxO9pKgrp=)CHxkt
zc}C=-67!eq@G>7=c*r_jm=D0pkA~y_uvaktujUK}R(gj2+ML0_#Pa`5^hnxa|B>k7
z5Hx%6`V9l&nUe9*^n-ypz$HupMM}kO(HE0^aSs8TztPCM0F589dLd$;cZ|wDTN22onep_6mmE|45@BbPlc~|^+P4|92`TmG*
z%>CL#-uq4K>m||KQ}^Ri$m?bGX0KT)*2_l>|MSuH_int_o1xXi1uyaGXoXG`ZfCe`
zJO0TfpO42WZze~A0;6fVlc8uYS1~WK5@L^h?xavAhK=2Nj0<#7H(Lm
zj#Io*oa1xuj|4=HKQDHQI}p0fBtd|<1OWY>grTseNI=vWj#ET&3DTL4`;|qIo@bd*
z=wPS7j)v=j-HUTO=gKzEB30!XrHweZrb2gSn&*<`_shPGl;heQtqv&lw+RyEuso!+
zwIq+ES&l$JDRtElX=i#;=s}p@NDD|VC==z+enmh^yKx(`&U`kayn5<>Q^08!i5sOp
zD6NLfaZWj$-tJOGeATeh5bBlQz-=jaeqV@SV2?Om*>>TqfHi;
zNOr5FytoGZux(aBt0dXx7+9^wM)R-)l4crM6^dQx3xAm)@vLhlS(DT0{R2vWaVDuG
zspl{K%ru#56bVEr0W5jSmiwo6_m?5(&_8oOV5!=v^9pMeqEP#QGSAJ;G>sX7Ni1YN
z8*cZLy`L#MJg0Sd8R?Bw2WwYpKN($rKjQ!PoKb%9@BW$!lFofZs0I%^Pg0qYvNQB>
zJ572pzfpWo#k1kG{EY-NcIqEBxA~N7#(-yM=*jl}!U4n5fgTQ4InAcFO5AP$fj`od
zeN%JV<>$COsLcOk&W;%HQ-3M}Z5S=8EbRpEs#QtH>?hF8-4>e)pHCXPC#G)eW9qW3
zbARn45C^ioeUPtu_ZK(z!&GAInhf;}IAC`*wT?nVTr6
zE_T_BV|g@E@sFK^nTlr6*!UuhBA57%bS-fBpwW$lP^ksM0`*xag*$^tOu}ZBG#Q}?
z9vLaARMo?$W||TRFWn6;empzOA)gIcH<}n}iycfv$FSkGqC!DQBU2Y%vJQEo^-^@=
z+^J-pcexOh!T7unrZzv@A1wkk9ju$-)b*bk=aLO
zFry!1cyR#!*;H98{E{INI`SGbQM~5UaxL(Lp9_&HH8>L+Gr?iaeB5rw(EONa6a>zWW9G;k`~dRgGIdFtvTiG^g2%?n)=~S+qDVblrkq=oXN^i^{R~u08Q&@Z7YS4`
z-tVNV_zD|=rN_Q0_j!?1D=DGWM)k_uMZ8~8Zz3#2#R%Do8rS01Cw%!Whbg){#0U+E
z<4!Ff)^JPy_d)oL%+@oD-TTA=8;>QJ%8NJCZTR0|d5XB#u@Ezcp1eeEjzancgq4iM
zP{i9CbQi=Cn2(gNtAWCl-cGEg<18NfjF*e#baaqtK%gK+eFDV%OE1&mpax(fF3RHhp7^17+J}rVp-1(ee++<;SCAd>Id89hCOU>qmBcy!T|R=
zrJTE~u~1vsa5;I~Un`CZ)vecEb0#hmc%jWK+2L&TD|V{AK8%Gs=7_N8-JeEzpUvxh@
z;o>Ce{QbGX*Ruim
zP3p(-?w$qDg~h96$vR0l6CW_5<0p;k#a1Gfg_}?RysU&hayZL+|OIL+$O)|7djXUh#~?o@zq7)M|VD#{TFhd8hI5(bx6!{++fBp9Uf7pBLfGV3U
zZj_Rg29e&R3ew%(64H{=A#7sPNViBzN_VG7ccXMjw{%NN-VNxhzIEPn?|063|2Qb@
zXFq$MS--Vrtu<@b%$_0Zbv-!(8Xv_@-=SG-DQfWwNjizStaNzdb~(y7=C(-RQZzK{
zb|nkrTI=vq?*Y`pkhlkf4xZgt<5BX~Sug!TX{>WQqkKuZrE|n7h1~3R^lD(dF&0md
zrsd!&Klj1M1WUvwra@VzL6}}uKAIBRqdc)>4Z-9#SOJ&^Fu1%lWDlaP1j;J*lE{co
z8JNB_mg}|eJ+w5V87uw>&1@{dzcQXp;Y);vj@b_d&7-Cz8_taQ7z0kSA*Is27hil(
zlppEtfiaVMSOog>B6ugHxz-kPO^UKr*jt!H^JuX)qBQFLQfHVTA9kH(S*LuE}6V$vJfVkeRZ!iIjr^RbE!wC7;i=5D1#wzuJ19D1f(^-l0Twg
z&ZB$F4y7@d++On}Y+XOe3Ye(HnzaUVx@3j6saI
zS&J3aiS!C{P;fk2TKwVci1`mmXi^+M8ZW?GdU)Z59PK(1N+E|z
zVx^Vd*&NverMWPOkQkg?c^46t4kv2DJ^Ix$hkTUy*oSaVy4%XE+s99e`K{AJYOtHd
z`BXN7d=nOUx#;fk95yy*DtKUWJ#8k2oEowuF+QnGqaZ?_;Of6jFnE97Mya@cMK%^xQks4jsj)O4FjW
z8zvJzHQ|ZCcZ7G?CrY9ZYUL_ekE$q8)joBEa6(Hg;1T&CnkVC2aI6f{o)q>o9%hB6
z(D}F}PW2voS0~f@g#|R4az8;Q6&^MM?r_RMG3;X~MZ!ixCmQR}+!Pjtyi@|0=ye_S
z!djQIE~IobggCxj8w4D8nO64(gFJOH#$*6Ns0e1#1N2pwRfCk`Ryx
zwgm`m)dqa(;oy_C#MD8#Earynb6sXeSLH8ZFCl`R{3nT6htRL4A{U{1l4Qq)&C>>X
zy3QRJIUm((=PpU&uhQ&j6zhy*CtAv#w7DM~@mNd)JK!l+#-B
z!Sdm_5Qi9s>OXx$$ohpGHb6_J^dJabV6knJ*k{F|(X&p}^0O?716kkeMP4=$YTK1c
z4Cg#Ahn5fhT>>`Nv+Y9^!^+}*IFes4-m3O`leuW(tUPRskH5c)u6zGE^+L*!XhA!n
zu-tBN|9g=Qt8yz7l{a`yq$6IzP875yIy5$5m&b5A%t`b
zWB@^PU2-jt$#WNM>{a!2%M)&E--54Xi-bJ&zJMS;*
z&y<^IqpG8$4$cA59-1C`voQmn0u5@kzK|KDPXcO>KWX4ktXv@TFw#ewl6x6Q#$(3}
z(ju=$pBA#x8weZOaB8j;!s6m2cc*k5tBQFlB+LyI?#P8i)Y=2@K7M>!&*udFW-;aS
zcmPWV{Jne|?&ZRwWd{g$8+sx;S?t3AmRi%ZV2|)O(`e>m8|Lmc99%9;i)>7;OpDsd
zxY3ynpDYt`qYW7s-`VA|EHXYZe7Su(Xvpn^k%sFxEW`xt)^7*Rg1RdiR+ijgnSi#F
zGc?^NQ-|-n**{!0f*Nv|N3AEcD>=`WC!XXG8x!}Ijs}hB6zKSro5Ud`V69a86_uhIwGnW*=*fx;gG
zS*HqBOToJk1xhd5sv}t&F2O(a#f`NNbe6OT!x=1RZ+}-4ny~ri=~<=N`$`{9YhhKm
zAy0jqD&V02QqN-%d7(Y-e9)Z>?F?S=n&9I@0Y_lPY4U~;w1i)Rx>_=i&e^&kbQLuf
zq)(2W4at7;7d;!>muTg0$=g
zOskJ38vMPpOjRipRY9=+Ro0FI>6=CVTWMjUH$lo|bYP6toE!_Er!&ss^J{5*f%+
z2LwagG-XH1jSxjeu7)o*gp17`KfL}w3#f#-^n<$eGd*+;ZBSC#dtSHZl;vo|lG1?T
z(82bx>p{}l;O+wT1G^0+B>{&+IVu$26|sV
zh@LvMv(DkHig*jFv5VzTRLo1i=8Sf+D^F4#KXxMTH>#GzX3f-&H|zKY@o_QpLUG}`
zkcsfDXNf+Ho49v{@NyA?#d7s1KDe)sDq1OLkgq?3|c(k94F-%~-3rYIGo2
zv}*|?bSmWmOO23$lVYoYSHjyuO9JTnP=FcFl`vTIm$S0_0jDA#)V17?6Ov|~;)(1V
z%nZOgKR^O*Kq(#t1F_dM^3EZ{<_
zn}~q-dAQ_F#C;I^`hsGGS%%9)R7s@yT~GFn9wlB9v4t*l>;Qc$jI8vWBVik4y`CS^
zN1Qe74#;W;@D19v5KoHGHDu(-dMo(ivh>;vz!jZ+06;$W*;h9neCV~P$R|)je8R!q
zUX4!52X(*!O&$OdRHTyNQh`oPl+mwj#*4%po)E2Q4QH|GKTpy#+F6f(8be1hHolF4td}AD1>Y
zJKvAqgVw(n`Y{4C2Ikdsj%V9w8-
ziCBiJE~+t5{Ithq;*8oLKo+B^f-`A`G3KhOp~I#+xJ;^Sa<
z>_Qm6m2?yAIvFZ3J1umx+yg)LsUfPqLk=>ibMkzW__|>{T
zgk^2^+Ls_fgjU13H#yQmhFvkra
zhTy|#Hj5Pt0zB-#uO@4TE<@&l^AyXoD~JLym1zmO8U!EQ+@B+qE0yBJZ(C3ylb+b^
zIha7S{Vu&QeA#Wes@n8i
ztg>nA3wo{jd#M0+pJJhpz^i9J^{&uqY9rv|9G=Ddn9}4ujMA^unZEOh1vkm77azm905*?NZ%$ESAt2
zCA}>Bgzl}&kJG~2&oiaH`HTQWFOIQ^fU{AY&Mq|KZe;jqrLL^j@C>Na4FPz@f&I|~
z8FmQXviNIm9i;CF8z-=!9u|d%WvP!g37eJ~gRijaP8B>*wY5xOs&%Tm%;jAz0n@<2
z@>COzHR(oYoopWSmsnAP!8gyI=E=T$$BmQ5h+#@DQa~`EtS|bR-%+9v{8a|S==$yP
zm+qwKb`zJny~`b(&0V|?pRZ~Z-8XY-+0ILOLsh|#+%g4iU;uQPK+Z%3K`UD`zTWVW
z+U_jBT36aGy;p+Au=csoVKQj6Puy`#v%ae>iz_|ObV&QlJ(rkS(tYDsaA^k_{Rw-#AAebsdu#Eqw5vq3$2`g~@8t}kD<-OKU$f|#eHCEYv5m#NMY5U9~^r9r7fDqdfS
zHr?QsA!i8O_tjQ)k4=1iCmvURH2s{tQr*3?v+G+|AqJ)KqWnD)Q&>R1Qb!OQU6h;Q
zr}XT4m^%O0f{LGM6fj4#)!pA#8FozUxG-7chHAVvPpXH-6c6@
zm(fHQCmUr}6^TLVo
z8h3-r6n;wZg^BwBOV!LgmTG*JpsLUNR#Y7#GQC=+GkMI
z%&=uet?Wj?j_qkLAH4k$_?p
zHM_)WAUDPGU_@zb)ZRIikcXxt$fVL^%O|0ibxd1D*>ii1&B^z$VfM2(5wfZsN!J^@
zk-L16O{z$=S0p2fm{B#yUT$KMQ|UTEoInpQzVm~~CK6-I@kdGKtGP3#eIrh_{m`{Z^`!cmSU#+hN6fDbWm}&GeXT*QG4#`czgn9(m=EsrI>>u&vk^{>X*0GC!SBf}
zYsGaGS3oVrJaD1yxrIycXMNuv*F3WQ>x>91cuUfs_Z9_j-||~Vg#SJ-1%@h2ae|e*
zh9?Z{5y9p*3_PO%k>ACc)cOETz&W`x+XxMQVpHSWQt^AwWR(K<^_TM@Nq5ecb_$`JTu--2
zE>D_?o=~^6SX_89QSrXo7djtlxNLGooo>5&O?PoP3vp`2`l>S+gQV1J*FC+TcOMPy
zUN%0knmQ{7f!a@JVws#0dFfOHw@Cu0E(53l-BIQT&80EySPYff$b>5GBuEZ+8
zn8Ko3z`>MSz@40Lb+x+WHTU<+05EFdL56
zsKzqzeI_=8fe#*JvYBI){b9}^e~O#Z@6z?bchhP!z;~ZA-rfbKUrv)E4+zl?O34;&
zZbUaK`PR7_mX3~IgpATnzcU`+a2wEB}8JXE1XwlGUeHQkwCMrhPT9o4{}PMlY^
zv81R{mdV{e>m#gvW}`>3dRgN2PPXWRz_axcGn`%r2eT$w1%cuc9CBsZptL2M%b6iZ
z*JqIwuFCBd_O&q_yXRZ|9`^eqQuGi^a>?5COy0$2Uv{JrW=3siaOl1sqbKGX!8LfN
zEtv?g*);(y2_dImg1@{(5?>mgBgCKwX(+5LvLnQe#4%L+`Oz1RN=ttmFCwbM<7B^c
z`R2J=bvz>>nSx&3t`9=tq=S$KLZ5)fIZ~1Lgo4{V0UXF}L4x8?cr(ZL<#sK8gK7~UHryd!rYp|nkR!f|MJ
zSyO}~PMO868#jb;ysCMHLrU&xqg>1mycVacs!)^3Z~~hVk>AMd^Xdgc2-`l(*F2rdRaNdD>{hpKgUn
zRv(vX-csam@}#&+rN`vWtR7ADTGpGIV*W4bG@j_PTQOJ~SyBsC2yvl`I1dtxxCDey
zY{WT>Fym@)`!D^yxyK-nRkng?=>%;bs`JB_MQu3q03LA;r3ySo#^ZRh}7cf&8
z`z;-Yg~%>|7ZS?lo##siZ}F^!IgKiKn9`Y4>2e2WR62w<1E$0e;5)f4<%whBvjsQA
zk>ewm=>vsg`s1|i_+@ZtS?;GkC3=|_GYgls9VstzikvSs4bZ59AU#B={E{&i^&$sd
z8mLtZVkXuxnjyo$-i!l8NgX$6EcaLQ(m1C}@(k2YeVkAk9O&pq-O*gKbg6bh*A=!J
zRJO)dt4?m|Ru#@UrFha31&BA{$Bh@rS-0hL#xz=@s2F5ril402u!xaKcZD>SaCv}u
zZd&Yl2;CWp@qlsp3G+MCtlEGw54D1~QXiGo1UZ3|_pf4@SuVoUV}o%Z*c?xi#}(LD
zXH&b8I_K^Zl(V#neo7ue!;kX*(SvFUV*K|?N%N&a
zY6iAjUM|e8vMyT}ZKtQh2SIBaFtksvLLl7ew?hZ0iAIyZroMutr)||9`ZSBS!o{B4
z7Kt26n_W7e@tUGw(OgB&tIBbt9*<#pMrUbhQcm#wCfoQfhSz!jr96|5qP@TI7c}gr
z#}64?dhd_CLl(uQJWMQaGS)pD8fT4=GWBVn3svie@^UdL)R;x-Ll7xWul%=Ez{%sz4+?-Qdd7;d-lxmqiB#C;J$G#k{_OY5?*XmjMC6clVj}b%JEXwPeIwJdg1nRas(u`7F
zoD^2jX>>WES<>~oMRMSYY3dFa2YD8#Ej)%=aXVsKUSy?$nX`xE3nB132CG;}N5bW>
z4$C9lTWOI#XnEM%nnx~`r&o0!Y3;Z|oen)PbGcm2MX;@tpFTq0gM!aRR<8&Xxu?oi
z#;VVeS2jA%@zIe0^mxim=JRp5xq?O%Po
z?;|l;Khm79#+6FFjX7x(vw6R;rb_)uEpG;UGvr200K8Mym81;$ju6P-Ox`5UpDT!BsiNNWU5rWQ*nw1DXhN4VnFv~wx8eASa;g73*AeY
zMrMFP*>)U>8PfRLE}Kz!QnosS@1DS>sZT4?bu?I`M7ga!&DEq9i-vBVzhU+`NA#i@
zVB13zG@So*nhnuzVo5YBRmmjH@3cm>`%j(&>O?P>Z@dg(a%w-HWhi4J^s?yd2<8ZZ413X2}gqCF+4F2
zCa0JBL22Eonpy)(p~%9`QHP|_6MS;ty65N0u@QHNzTEm!{f39V%e{IdYmJf${Mi2>_TDK()U
zN!Z{`(VjACDGvQlvN9;p$+{P&MmDTPqJ_5AM%LJXWZk}exv+VH+;-UFI(335U0Yn~)q#PHQUQISjF
z^cq>p9=hw3s;i}dKFxkXvFA2=UNws{D-sj{4-E~+_oQsVGs|^Nx}Kf2D>v63XOlk(
zMVWuM%{>EncJ5z>q72LozfR6gtEmU7vt56mtRF=3s!$9gqIsg#qdtPhmp8f$!5e7u
z#_#-Mm0)eGBfMTOXjG3UlH)qel}75P~+))~j8i++*Y*A@FF-j|82Xq%%IaeIpa=C9va?W*88q_wB8Bexg1
zo_SmlR^VU7K5#i@@+8VqXkC9qCY%UBXKUJ|Cnga4s&8(_xyv3?9~V4BG`3Oq{6lez
zGbJX{V8^@(9#RL2TQRYZ94cQ4c<(?S;>~1HAVuZ4^ll1-I-3U}>Ws0yCv{x8l?FB4
zh)KPqrNTD!+_JRii^9Gmoa%sJ#4Ym(le9F<_&Sd6$Kfa?ohOb1p{#G(3=QMvJn0w`
z9#JRP-(fjJFs*#!ieOjJ6?bzR-*P9d>S>VqVBjj&+f1_e3t`1R){nC*iy0@n#V>UW
zp1j=CfU`1$nPlb0*G9&9p5*cXNNY(PT{z(Ln1TwTqW5F_gyiu=l#bEId-L_{DWr#K
zPWK(@4&|&P{VZd{U+%h8LmF&x_Ax>qv&Ov8FUX(&)CzEV_NfyzM&thAz#JE)pH(^p
zYFc$t1j>cnJg5`yK_N%8sdac+nTF`c-3KSZzKH$|Sa(5+g>4X(sl!!*kM6Zl!oRz`
zABQPgA0c~ix97fc6BD!z;t+O}(3bo(7
zp%(6&Z<98lstmkUgbt(!+$IO=cshJu+L{)^WanLCwcFFylhf26-Qnvw!((n+QCf
zf`PBFkq|Fhw^+D9@7*eFatA;K|32ft(*f=coAjdKcbi>e(bAI%0^)FBes0
zCy)*zxWh