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 -![test.svg](test.svg) - -### Should only convert it once -![test.svg](test.svg) - -### Download remote images -![remote image](http://upload.wikimedia.org/wikipedia/commons/b/b0/NewTux.svg) - -### Remote images with same filename -![youtube1](http://img.youtube.com/vi/9bZkp7q19f0/0.jpg) -![youtube2](http://img.youtube.com/vi/IkV2HQLAKHY/0.jpg) - 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 -![test.svg](../test.svg) - 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('
\n

test**hello**test

\n
'); - }); - }); - }); -}); - 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': '![image](test.svg)', + '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': '![image](test.svg)', - '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': '![image](test.svg)', - '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': '![image](test.svg)', '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': '![image](test.svg)', + 'svg_inline.md': 'This is a svg: '+SVG, + 'test.svg': '' + SVG, + + // test for remote files + 'remote_png.md': '![image](https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png)', + 'remote_svg.md': '![image](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg)', + + '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': '![image](test.svg)', - '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('
\n
\n
jshello worldtest
\n
\n
'); + }); + }); + 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>" ) + 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>" ); - - 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( "