diff options
Diffstat (limited to 'lib')
41 files changed, 328 insertions, 3267 deletions
diff --git a/lib/api/index.js b/lib/api/index.js deleted file mode 100644 index 213ef1a..0000000 --- a/lib/api/index.js +++ /dev/null @@ -1,5 +0,0 @@ - -module.exports = { - encodePage: require('./encodePage'), - decodePage: require('./decodePage') -}; diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js new file mode 100644 index 0000000..eed5e5f --- /dev/null +++ b/lib/json/encodeBook.js @@ -0,0 +1,21 @@ +var encodeSummary = require('./encodeSummary'); +var encodeGlossary = require('./encodeGlossary'); +var encodeReadme = require('./encodeReadme'); +var encodeConfig = require('./encodeConfig'); + +/** + Encode a book to JSON + + @param {Book} + @return {Object} +*/ +function encodeBookToJson(book) { + return { + summary: encodeSummary(book.getSummary()), + glossary: encodeGlossary(book.getGlossary()), + readme: encodeReadme(book.getReadme()), + config: encodeConfig(book.getConfig()) + }; +} + +module.exports = encodeBookToJson; diff --git a/lib/json/encodeBookWithPage.js b/lib/json/encodeBookWithPage.js new file mode 100644 index 0000000..26ab174 --- /dev/null +++ b/lib/json/encodeBookWithPage.js @@ -0,0 +1,22 @@ +var encodeBook = require('./encodeBook'); +var encodePage = require('./encodePage'); +var encodeFile = require('./encodeFile'); + +/** + Return a JSON representation of a book with a specific file + + @param {Book} output + @param {Page} page + @return {Object} +*/ +function encodeBookWithPage(book, page) { + var file = page.getFile(); + + var result = encodeBook(book); + result.page = encodePage(page); + result.file = encodeFile(file); + + return result; +} + +module.exports = encodeBookWithPage; diff --git a/lib/json/encodeConfig.js b/lib/json/encodeConfig.js new file mode 100644 index 0000000..195d43b --- /dev/null +++ b/lib/json/encodeConfig.js @@ -0,0 +1,12 @@ +/** + Encode configuration to JSON + + @param {Config} + @return {Object} +*/ +function encodeConfig(config) { + var values = config.getValues(); + return values.toJS(); +} + +module.exports = encodeConfig; diff --git a/lib/json/encodeFile.js b/lib/json/encodeFile.js new file mode 100644 index 0000000..03b3450 --- /dev/null +++ b/lib/json/encodeFile.js @@ -0,0 +1,16 @@ + +/** + Return a JSON representation of a file + + @param {File} file + @return {Object} +*/ +function encodeFileToJson(file) { + return { + path: file.getPath(), + mtime: file.getMTime(), + type: file.getType() + }; +} + +module.exports = encodeFileToJson; diff --git a/lib/json/encodeGlossary.js b/lib/json/encodeGlossary.js new file mode 100644 index 0000000..fcaf4d2 --- /dev/null +++ b/lib/json/encodeGlossary.js @@ -0,0 +1,21 @@ +var encodeFile = require('./encodeFileToJson'); +var encodeGlossaryEntry = require('./encodeGlossaryEntry'); + +/** + Encode a glossary to JSON + + @param {Glossary} + @return {Object} +*/ +function encodeGlossary(glossary) { + var file = glossary.getFile(); + var entries = glossary.getEntries(); + + return { + file: encodeFile(file), + entries: entries + .map(encodeGlossaryEntry).toJS() + }; +} + +module.exports = encodeGlossary; diff --git a/lib/json/encodeGlossaryEntry.js b/lib/json/encodeGlossaryEntry.js new file mode 100644 index 0000000..d163f45 --- /dev/null +++ b/lib/json/encodeGlossaryEntry.js @@ -0,0 +1,16 @@ + +/** + Encode a SummaryArticle to JSON + + @param {GlossaryEntry} + @return {Object} +*/ +function encodeGlossaryEntry(entry) { + return { + id: entry.getID(), + name: entry.getName(), + description: entry.getDescription() + }; +} + +module.exports = encodeGlossaryEntry; diff --git a/lib/json/encodePage.js b/lib/json/encodePage.js new file mode 100644 index 0000000..d876c78 --- /dev/null +++ b/lib/json/encodePage.js @@ -0,0 +1,30 @@ +/** + Return a JSON representation of a page + + @param {Page} page + @param {Summary} summary + @return {Object} +*/ +function encodePage(page, summary) { + var file = page.getFile(); + var attributes = page.getAttributes(); + var article = summary.getByPath(file.getPath()); + + + var result = attributes.toJS(); + + if (article) { + result.title = article.getTitle(); + result.level = article.getLevel(); + result.depth = article.getDepth(); + + // todo: next and prev + } + + result.content = page.getContent(); + result.dir = page.getDir(); + + return result; +} + +module.exports = encodePage; diff --git a/lib/json/encodeReadme.js b/lib/json/encodeReadme.js new file mode 100644 index 0000000..0127788 --- /dev/null +++ b/lib/json/encodeReadme.js @@ -0,0 +1,17 @@ +var encodeFile = require('./encodeFileToJson'); + +/** + Encode a readme to JSON + + @param {Readme} + @return {Object} +*/ +function encodeReadme(readme) { + var file = readme.getFile(); + + return { + file: encodeFile(file) + }; +} + +module.exports = encodeReadme; diff --git a/lib/json/encodeSummary.js b/lib/json/encodeSummary.js new file mode 100644 index 0000000..97db910 --- /dev/null +++ b/lib/json/encodeSummary.js @@ -0,0 +1,20 @@ +var encodeFile = require('./encodeFile'); +var encodeSummaryPart = require('./encodeSummaryPart'); + +/** + Encode a summary to JSON + + @param {Summary} + @return {Object} +*/ +function encodeSummary(summary) { + var file = summary.getFile(); + var parts = summary.getParts(); + + return { + file: encodeFile(file), + parts: parts.map(encodeSummaryPart).toJS() + }; +} + +module.exports = encodeSummary; diff --git a/lib/json/encodeSummaryArticle.js b/lib/json/encodeSummaryArticle.js new file mode 100644 index 0000000..485d209 --- /dev/null +++ b/lib/json/encodeSummaryArticle.js @@ -0,0 +1,18 @@ + +/** + Encode a SummaryArticle to JSON + + @param {SummaryArticle} + @return {Object} +*/ +function encodeSummaryArticle(article) { + return { + title: article.getTitle(), + level: article.getLevel(), + depth: article.getDepth(), + articles: article.getArticles() + .map(encodeSummaryArticle).toJS() + }; +} + +module.exports = encodeSummaryArticle; diff --git a/lib/json/encodeSummaryPart.js b/lib/json/encodeSummaryPart.js new file mode 100644 index 0000000..a5e7218 --- /dev/null +++ b/lib/json/encodeSummaryPart.js @@ -0,0 +1,17 @@ +var encodeSummaryArticle = require('./encodeSummaryArticle'); + +/** + Encode a SummaryPart to JSON + + @param {SummaryPart} + @return {Object} +*/ +function encodeSummaryPart(part) { + return { + title: part.getTitle(), + articles: part.getArticles() + .map(encodeSummaryArticle).toJS() + }; +} + +module.exports = encodeSummaryPart; diff --git a/lib/json/index.js b/lib/json/index.js new file mode 100644 index 0000000..a026a7f --- /dev/null +++ b/lib/json/index.js @@ -0,0 +1,10 @@ + +module.exports = { + encodeBookWithPage: require('./encodeBookWithPage'), + encodeBook: require('./encodeBook'), + encodeFile: require('./encodeFile'), + encodePage: require('./encodePage'), + encodeSummary: require('./encodeSummary'), + encodeReadme: require('./encodeReadme'), + encodeConfig: require('./encodeConfig') +}; diff --git a/lib/models/file.js b/lib/models/file.js index ebfe629..b6b06ee 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -20,6 +20,20 @@ File.prototype.getMTime = function() { }; /** + Return type of file ('markdown' or 'asciidoc') + + @return {String} +*/ +File.prototype.getType = function() { + var parser = this.getParser(); + if (parser) { + return parser.name; + } else { + return undefined; + } +}; + +/** Return extension of this file (lowercased) @return {String} diff --git a/lib/models/page.js b/lib/models/page.js index 1ac0d50..0a9cf38 100644 --- a/lib/models/page.js +++ b/lib/models/page.js @@ -9,7 +9,10 @@ var Page = Immutable.Record({ attributes: Immutable.Map(), // Content of the page - content: String() + content: String(), + + // Direction of the text + dir: String('ltr') }); Page.prototype.getFile = function() { @@ -24,6 +27,10 @@ Page.prototype.getContent = function() { return this.get('content'); }; +Page.prototype.getDir = function() { + return this.get('dir'); +}; + /** Create a page for a file diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index 4a448c8..e3d85ef 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -30,6 +30,15 @@ SummaryArticle.prototype.getArticles = function() { }; /** + Return how deep the article is + + @return {Number} +*/ +SummaryArticle.prototype.getDepth = function() { + return this.getLevel().split('.').length; +}; + +/** Return an article by its level @param {String} level diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index 3ef5c74..a84e297 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -1,4 +1,6 @@ +var JSONUtils = require('../../json'); var Modifier = require('../modifier'); +var Writer = require('../writer'); /** Write a page as a json file @@ -7,13 +9,22 @@ var Modifier = require('../modifier'); @param {Page} page */ function onPage(output, page) { - var options = output. - - return Modifier.applyHTMLTransformations(page, [ + return Modifier.modifyHTML(page, [ Modifier.HTML.resolveLinks() ]) - .then(function(newPage) { + .then(function(resultPage) { + // Generate the JSON + var json = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + // Write it to the disk + return Writer.writePage( + output, + page, + JSON.stringify(json, null, 4), + { + extension: '.json' + } + ); }); } diff --git a/lib/output/modifiers/index.js b/lib/output/modifiers/index.js index 76ce3c2..ced8716 100644 --- a/lib/output/modifiers/index.js +++ b/lib/output/modifiers/index.js @@ -1,9 +1,4 @@ - -function modifyPage() { - - -} - - -module.exports = modifyPage; +module.exports = { + modifyHTML: require('./modifyHTML') +}; diff --git a/lib/output/modifiers/modifyHTML.js b/lib/output/modifiers/modifyHTML.js new file mode 100644 index 0000000..c1dad74 --- /dev/null +++ b/lib/output/modifiers/modifyHTML.js @@ -0,0 +1,15 @@ + + +/** + Apply a list of operations to a page and + output the new page. + + @param {Page} + @param {List<Transformation>} +*/ +function modifyHTML(page, operations) { + +} + + +module.exports = modifyHTML; diff --git a/lib/output/writer/index.js b/lib/output/writer/index.js new file mode 100644 index 0000000..2ef3364 --- /dev/null +++ b/lib/output/writer/index.js @@ -0,0 +1,4 @@ + +module.exports = { + writePage: require('./writePage') +}; diff --git a/lib/output/writer/writePage.js b/lib/output/writer/writePage.js new file mode 100644 index 0000000..23e37d0 --- /dev/null +++ b/lib/output/writer/writePage.js @@ -0,0 +1,33 @@ +var path = require('path'); +var Immutable = require('immutable'); + +var fs = require('../../utils/fs'); +var PathUtils = require('../../utils/path'); + +var WriteOptions = Immutable.Record({ + extension: '.html' +}); + +/** + Write a file to the output folder + + @param {Output} output + @param {Page} page + @param {Buffer|String} content + @return {Promise} +*/ +function writePage(output, page, content, options) { + var file = page.getFile(); + var outputOpts = output.getOptions(); + var rootFolder = outputOpts.get('root'); + + options = WriteOptions(options); + + // Get filename for file to write + var fileName = PathUtils.setExtension(file.getPath(), options.get('extension')); + var filePath = path.join(rootFolder, fileName); + + return fs.writeFile(filePath, content); +} + +module.exports = writePage; diff --git a/lib/output2/assets-inliner.js b/lib/output2/assets-inliner.js deleted file mode 100644 index 6f1f02d..0000000 --- a/lib/output2/assets-inliner.js +++ /dev/null @@ -1,140 +0,0 @@ -var util = require('util'); -var path = require('path'); -var crc = require('crc'); - -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'; - -/* -Mixin to inline all the assets in a book: - - Outline <svg> tags - - Download remote images - - Convert .svg images as png -*/ - -module.exports = function assetsInliner(Base) { - Base = Base || FolderOutput; - - function AssetsInliner() { - Base.apply(this, arguments); - - // Map of svg already converted - this.svgs = {}; - this.inlineSvgs = {}; - - // Map of images already downloaded - this.downloaded = {}; - } - util.inherits(AssetsInliner, Base); - - // 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() { - // Resolve src to a relative filepath to the book's root - src = page.resolveLocal(src); - - // Already a PNG/JPG/.. ? - if (path.extname(src).toLowerCase() != '.svg') { - return src; - } - - // Convert SVG to PNG - return that.convertSVGFile(that.resolve(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]); - - 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); - }); - }; - - return AssetsInliner; -}; diff --git a/lib/output2/base.js b/lib/output2/base.js deleted file mode 100644 index 868b85b..0000000 --- a/lib/output2/base.js +++ /dev/null @@ -1,309 +0,0 @@ -var _ = require('lodash'); -var Ignore = require('ignore'); -var path = require('path'); - -var Promise = require('../utils/promise'); -var pathUtil = require('../utils/path'); -var location = require('../utils/location'); -var error = require('../utils/error'); -var PluginsManager = require('../plugins'); -var TemplateEngine = require('../template'); -var gitbook = require('../gitbook'); - -/* -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, opts, parent) { - _.bindAll(this); - this.parent = parent; - - this.opts = _.defaults({}, opts || {}, { - directoryIndex: true - }); - - this.book = book; - book.output = this; - this.log = this.book.log; - - // Create plugins manager - this.plugins = new PluginsManager(this.book); - - // Create template engine - this.template = new TemplateEngine(this); - - // Files to ignore in output - this.ignore = Ignore(); - - // Hack to inherits from rules of the book - this.ignore.add(this.book.ignore); -} - -// Default name for generator -Output.prototype.name = 'base'; - -// Default extension for output -Output.prototype.defaultExtension = '.html'; - -// Start the generation, for a parsed book -Output.prototype.generate = function() { - var that = this; - var isMultilingual = this.book.isMultilingual(); - - return Promise() - - // Load all plugins - .then(function() { - return that.plugins.loadAll() - .then(function() { - that.template.addFilters(that.plugins.getFilters()); - that.template.addBlocks(that.plugins.getBlocks()); - }); - }) - - // Transform the configuration - .then(function() { - return that.plugins.hook('config', that.book.config.dump()) - .then(function(cfg) { - that.book.config.replace(cfg); - }); - }) - - // Initialize the generation - .then(function() { - return that.plugins.hook('init'); - }) - .then(function() { - that.log.info.ln('preparing the generation'); - return that.prepare(); - }) - - // Process all files - .then(function() { - that.log.debug.ln('listing files'); - return that.book.fs.listAllFiles(that.book.root); - }) - - // We want to process assets first, then pages - // Since pages can have logic based on existance of assets - .then(function(files) { - // Split into pages/assets - var byTypes = _.chain(files) - .filter(that.ignore.createFilter()) - - // 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) { - that.log.debug.ln('copy asset', filename); - return that.onAsset(filename); - }) - .then(function() { - return Promise.serie(byTypes.page, function(filename) { - that.log.debug.ln('process page', filename); - return that.onPage(that.book.getPage(filename)); - }); - }); - }) - - // Generate sub-books - .then(function() { - if (!that.book.isMultilingual()) return; - - return Promise.serie(that.book.books, function(subbook) { - that.log.info.ln(''); - that.log.info.ln('start generation of language "' + path.relative(that.book.root, subbook.root) + '"'); - - var out = that.onLanguageBook(subbook); - return out.generate(); - }); - }) - - // Finish the generation - .then(function() { - return that.plugins.hook('finish:before'); - }) - .then(function() { - that.log.debug.ln('finishing the generation'); - return that.finish(); - }) - .then(function() { - return that.plugins.hook('finish'); - }) - - .then(function() { - if (!that.book.isLanguageBook()) that.log.info.ln(''); - that.log.info.ok('generation finished with success!'); - }); -}; - -// Prepare the generation -Output.prototype.prepare = function() { - this.ignore.addPattern(_.compact([ - '.gitignore', - '.ignore', - '.bookignore', - 'node_modules', - '_layouts', - - // 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 -Output.prototype.onPage = function(page) { - return page.toHTML(this); -}; - -// Copy an asset file (non-parsable), ex: images, etc -Output.prototype.onAsset = function(filename) { - -}; - -// Finish the generation -Output.prototype.finish = function() { - -}; - -// Resolve an HTML link -Output.prototype.onRelativeLink = function(currentPage, href) { - var to = currentPage.followPage(href); - - // Replace by an .html link - if (to) { - href = to.path; - - // Change README path to be "index.html" - if (href == this.book.readme.path) { - href = 'index.html'; - } - - // Recalcul as relative link - href = currentPage.relative(href); - - // Replace .md by .html - href = this.toURL(href); - } - - return href; -}; - -// Output a SVG buffer as a file -Output.prototype.onOutputSVG = function(page, svg) { - return null; -}; - -// Output an image as a file -// Normalize the relative link -Output.prototype.onOutputImage = function(page, imgFile) { - if (location.isExternal(imgFile)) { - return imgFile; - } - - imgFile = page.resolveLocal(imgFile); - return page.relative(imgFile); -}; - -// 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); -}; - -// Prepare output for a language book -Output.prototype.onLanguageBook = function(book) { - return new this.constructor(book, this.opts, this); -}; - - -// ---- Utilities ---- - -// Return conetxt for the output itself -Output.prototype.getSelfContext = function() { - return { - name: this.name - }; -}; - -// Return a default context for templates -Output.prototype.getContext = function() { - var ctx = _.extend( - { - output: this.getSelfContext() - }, - this.book.getContext(), - (this.book.isLanguageBook()? this.book.parent: this.book).langs.getContext(), - this.book.readme.getContext(), - this.book.summary.getContext(), - this.book.glossary.getContext(), - this.book.config.getContext(), - gitbook.getContext() - ); - - // Deprecated fields - error.deprecateField(ctx.gitbook, 'generator', this.name, '"gitbook.generator" property is deprecated, use "output.name" instead'); - - return ctx; -}; - -// Resolve a file path in the context of a specific page -// Result is an "absolute path relative to the output folder" -Output.prototype.resolveForPage = function(page, href) { - if (_.isString(page)) page = this.book.getPage(page); - - href = page.relative(href); - return this.onRelativeLink(page, href); -}; - -// Filename for output -// READMEs are replaced by index.html -// /test/README.md -> /test/index.html -Output.prototype.outputPath = function(filename, ext) { - ext = ext || this.defaultExtension; - var output = filename; - - if ( - path.basename(filename, path.extname(filename)) == 'README' || - output == this.book.readme.path - ) { - output = path.join(path.dirname(output), 'index'+ext); - } else { - output = pathUtil.setExtension(output, ext); - } - - return output; -}; - -// Filename for output -// /test/index.html -> /test/ -Output.prototype.toURL = function(filename, ext) { - var href = this.outputPath(filename, ext); - - if (path.basename(href) == 'index.html' && this.opts.directoryIndex) { - href = path.dirname(href) + '/'; - } - - return location.normalize(href); -}; - -module.exports = Output; diff --git a/lib/output2/conrefs.js b/lib/output2/conrefs.js deleted file mode 100644 index e58f836..0000000 --- a/lib/output2/conrefs.js +++ /dev/null @@ -1,67 +0,0 @@ -var path = require('path'); -var util = require('util'); - -var folderOutput = require('./folder'); -var Git = require('../utils/git'); -var fs = require('../utils/fs'); -var pathUtil = require('../utils/path'); -var location = require('../utils/location'); - -/* -Mixin for output to resolve git conrefs -*/ - -module.exports = function conrefsLoader(Base) { - Base = folderOutput(Base); - - function ConrefsLoader() { - Base.apply(this, arguments); - - 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)) { - var href = location.toAbsolute(to, path.dirname(from), ''); - return this.book.resolve(href); - } - - // 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; -}; diff --git a/lib/output2/ebook.js b/lib/output2/ebook.js deleted file mode 100644 index 2b8fac9..0000000 --- a/lib/output2/ebook.js +++ /dev/null @@ -1,193 +0,0 @@ -var _ = require('lodash'); -var util = require('util'); -var juice = require('juice'); - -var command = require('../utils/command'); -var fs = require('../utils/fs'); -var Promise = require('../utils/promise'); -var error = require('../utils/error'); -var WebsiteOutput = require('./website'); -var assetsInliner = require('./assets-inliner'); - -function _EbookOutput() { - WebsiteOutput.apply(this, arguments); - - // ebook-convert does not support link like "./" - this.opts.directoryIndex = false; -} -util.inherits(_EbookOutput, WebsiteOutput); - -var EbookOutput = assetsInliner(_EbookOutput); - -EbookOutput.prototype.name = 'ebook'; - -// Return context for templating -// Incldue type of ebbook generated -EbookOutput.prototype.getSelfContext = function() { - var ctx = EbookOutput.super_.prototype.getSelfContext.apply(this); - ctx.format = this.opts.format; - - return ctx; -}; - -// Finish generation, create ebook using ebook-convert -EbookOutput.prototype.finish = function() { - var that = this; - if (that.book.isMultilingual()) { - return EbookOutput.super_.prototype.finish.apply(that); - } - - return Promise() - .then(function() { - return EbookOutput.super_.prototype.finish.apply(that); - }) - - // Generate SUMMARY.html - .then(function() { - return that.render('summary', 'SUMMARY.html', that.getContext()); - }) - - // Start ebook-convert - .then(function() { - return that.ebookConvertOption(); - }) - - .then(function(options) { - if (!that.opts.format) return; - - var cmd = [ - 'ebook-convert', - that.resolve('SUMMARY.html'), - that.resolve('index.'+that.opts.format), - command.optionsToShellArgs(options) - ].join(' '); - - return command.exec(cmd) - .progress(function(data) { - that.book.log.debug(data); - }) - .fail(function(err) { - if (err.code == 127) { - throw error.RequireInstallError({ - cmd: 'ebook-convert', - install: 'Install it from Calibre: https://calibre-ebook.com' - }); - } - - throw error.EbookError(err); - }); - }); -}; - -// Generate header/footer for PDF -EbookOutput.prototype.getPDFTemplate = function(tpl) { - var that = this; - var context = _.extend( - { - // Nunjucks context mapping to ebook-convert templating - page: { - num: '_PAGENUM_', - title: '_TITLE_', - section: '_SECTION_' - } - }, - this.getContext() - ); - - return this.renderAsString('pdf_'+tpl, context) - - // Inline css, include css relative to the output folder - .then(function(output) { - return Promise.nfcall(juice.juiceResources, output, { - webResources: { - relativeTo: that.root() - } - }); - }); -}; - -// Locate the cover file to use -// Use configuration or search a "cover.jpg" file -// For multi-lingual book, it can use the one from the main book -EbookOutput.prototype.locateCover = function() { - var cover = this.book.config.get('cover', 'cover.jpg'); - - // Resolve to absolute - cover = this.resolve(cover); - - // Cover doesn't exist and multilingual? - if (!fs.existsSync(cover)) { - if (this.parent) return this.parent.locateCover(); - else return undefined; - } - - return cover; -}; - -// Generate options for ebook-convert -EbookOutput.prototype.ebookConvertOption = function() { - var that = this; - - var options = { - '--cover': this.locateCover(), - '--title': that.book.config.get('title'), - '--comments': that.book.config.get('description'), - '--isbn': that.book.config.get('isbn'), - '--authors': that.book.config.get('author'), - '--language': that.book.config.get('language'), - '--book-producer': 'GitBook', - '--publisher': 'GitBook', - '--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]', - '--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]', - '--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]', - '--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]', - '--no-chapters-in-toc': true, - '--max-levels': '1', - '--breadth-first': true - }; - - if (that.opts.format == 'epub') { - options = _.extend(options, { - '--dont-split-on-page-breaks': true - }); - } - - if (that.opts.format != 'pdf') return Promise(options); - - var pdfOptions = that.book.config.get('pdf'); - - options = _.extend(options, { - '--chapter-mark': String(pdfOptions.chapterMark), - '--page-breaks-before': String(pdfOptions.pageBreaksBefore), - '--margin-left': String(pdfOptions.margin.left), - '--margin-right': String(pdfOptions.margin.right), - '--margin-top': String(pdfOptions.margin.top), - '--margin-bottom': String(pdfOptions.margin.bottom), - '--pdf-default-font-size': String(pdfOptions.fontSize), - '--pdf-mono-font-size': String(pdfOptions.fontSize), - '--paper-size': String(pdfOptions.paperSize), - '--pdf-page-numbers': Boolean(pdfOptions.pageNumbers), - '--pdf-header-template': that.getPDFTemplate('header'), - '--pdf-footer-template': that.getPDFTemplate('footer'), - '--pdf-sans-family': String(pdfOptions.fontFamily) - }); - - return that.getPDFTemplate('header') - .then(function(tpl) { - options['--pdf-header-template'] = tpl; - - return that.getPDFTemplate('footer'); - }) - .then(function(tpl) { - options['--pdf-footer-template'] = tpl; - - return options; - }); -}; - -// Don't write multi-lingual index for wbook -EbookOutput.prototype.outputMultilingualIndex = function() { - -}; - -module.exports = EbookOutput; diff --git a/lib/output2/folder.js b/lib/output2/folder.js deleted file mode 100644 index 8303ed2..0000000 --- a/lib/output2/folder.js +++ /dev/null @@ -1,152 +0,0 @@ -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 requires the native fs module to output -book as a directory (mapping assets and pages) -*/ - -module.exports = function folderOutput(Base) { - Base = Base || Output; - - function FolderOutput() { - Base.apply(this, arguments); - - this.opts.root = path.resolve(this.opts.root || this.book.resolve('_book')); - } - util.inherits(FolderOutput, Base); - - // 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() { - var that = this; - - return Promise() - .then(function() { - return FolderOutput.super_.prototype.prepare.apply(that); - }) - - // Cleanup output folder - .then(function() { - that.log.debug.ln('removing previous output directory'); - return fs.rmDir(that.root()) - .fail(function() { - return Promise(); - }); - }) - - // Create output folder - .then(function() { - that.log.debug.ln('creating output directory'); - return fs.mkdirp(that.root()); - }) - - // Add output folder to ignored files - .then(function() { - that.ignore.addPattern([ - path.relative(that.book.root, that.root()) - ]); - }); - }; - - // Prepare output for a language book - FolderOutput.prototype.onLanguageBook = function(book) { - return new this.constructor(book, _.extend({}, this.opts, { - - // Language output should be output in sub-directory of output - root: path.resolve(this.root(), book.language) - }), this); - }; - - // ----- Utility methods ----- - - // Return path to the root folder - FolderOutput.prototype.root = function() { - return this.opts.root; - }; - - // 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); - var folder = path.dirname(to); - - // Ensure folder exists - return fs.mkdirp(folder); - }) - .then(function() { - 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 folder 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; -}; diff --git a/lib/output2/json.js b/lib/output2/json.js deleted file mode 100644 index 7061141..0000000 --- a/lib/output2/json.js +++ /dev/null @@ -1,47 +0,0 @@ -var conrefsLoader = require('./conrefs'); - -var JSONOutput = conrefsLoader(); - -JSONOutput.prototype.name = 'json'; - -// Don't copy asset on JSON output -JSONOutput.prototype.onAsset = function(filename) {}; - -// Write a page (parsable file) -JSONOutput.prototype.onPage = function(page) { - var that = this; - - // Parse the page - return page.toHTML(this) - - // Write as json - .then(function() { - var json = page.getOutputContext(that); - - // Delete some private properties - delete json.config; - - // Specify JSON output version - json.version = '3'; - - return that.writeFile( - page.withExtension('.json'), - JSON.stringify(json, null, 4) - ); - }); -}; - -// At the end of generation, generate README.json for multilingual books -JSONOutput.prototype.finish = function() { - if (!this.book.isMultilingual()) return; - - // Copy README.json from main book - var mainLanguage = this.book.langs.getDefault().id; - return this.copyFile( - this.resolve(mainLanguage, 'README.json'), - 'README.json' - ); -}; - - -module.exports = JSONOutput; diff --git a/lib/output2/website/index.js b/lib/output2/website/index.js deleted file mode 100644 index 0a8618c..0000000 --- a/lib/output2/website/index.js +++ /dev/null @@ -1,225 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var util = require('util'); -var I18n = require('i18n-t'); - -var Promise = require('../../utils/promise'); -var location = require('../../utils/location'); -var fs = require('../../utils/fs'); -var conrefsLoader = require('../conrefs'); -var Output = require('../base'); -var setupTemplateEnv = require('./templateEnv'); - -function _WebsiteOutput() { - Output.apply(this, arguments); - - // Nunjucks environment - this.env; - - // Plugin instance for the main theme - this.theme; - - // Plugin instance for the default theme - this.defaultTheme; - - // Resources loaded from plugins - this.resources; - - // i18n for themes - this.i18n = new I18n(); -} -util.inherits(_WebsiteOutput, Output); - -var WebsiteOutput = conrefsLoader(_WebsiteOutput); - -// Name of the generator -// It's being used as a prefix for templates -WebsiteOutput.prototype.name = 'website'; - -// Load and setup the theme -WebsiteOutput.prototype.prepare = function() { - var that = this; - - return Promise() - .then(function() { - return WebsiteOutput.super_.prototype.prepare.apply(that); - }) - - .then(function() { - // This list is ordered to give priority to templates in the book - var searchPaths = _.pluck(that.plugins.list(), 'root'); - - // The book itself can contains a "_layouts" folder - searchPaths.unshift(that.book.root); - - // Load i18n - _.each(searchPaths.concat().reverse(), function(searchPath) { - var i18nRoot = path.resolve(searchPath, '_i18n'); - - if (!fs.existsSync(i18nRoot)) return; - that.i18n.load(i18nRoot); - }); - - that.searchPaths = searchPaths; - }) - - // Copy assets from themes before copying files from book - .then(function() { - if (that.book.isLanguageBook()) return; - - // Assets from the book are already copied - // Copy assets from plugins (start with default plugins) - return Promise.serie(that.plugins.list().reverse(), function(plugin) { - // Copy assets only if exists (don't fail otherwise) - var assetFolder = path.join(plugin.root, '_assets', that.name); - if (!fs.existsSync(assetFolder)) return; - - that.log.debug.ln('copy assets from theme', assetFolder); - return fs.copyDir( - assetFolder, - that.resolve('gitbook'), - { - deleteFirst: false, - overwrite: true, - confirm: true - } - ); - }); - }) - - // Load resources for plugins - .then(function() { - return that.plugins.getResources(that.name) - .then(function(resources) { - that.resources = resources; - }); - }); -}; - -// Write a page (parsable file) -WebsiteOutput.prototype.onPage = function(page) { - var that = this; - - // Parse the page - return page.toHTML(this) - - // Render the page template with the same context as the json output - .then(function() { - return that.render('page', that.outputPath(page.path), page.getOutputContext(that)); - }); -}; - -// Finish generation, create ebook using ebook-convert -WebsiteOutput.prototype.finish = function() { - var that = this; - - return Promise() - .then(function() { - return WebsiteOutput.super_.prototype.finish.apply(that); - }) - - // Copy assets from plugins - .then(function() { - if (that.book.isLanguageBook()) return; - return that.plugins.copyResources(that.name, that.resolve('gitbook')); - }) - - // Generate homepage to select languages - .then(function() { - if (!that.book.isMultilingual()) return; - return that.outputMultilingualIndex(); - }); -}; - -// ----- Utilities ---- - -// Write multi-languages index -WebsiteOutput.prototype.outputMultilingualIndex = function() { - var that = this; - - return that.render('languages', 'index.html', that.getContext()); -}; - -/* - Render a template as an HTML string - Templates are stored in `_layouts` folders - - - @param {String} tpl: template name (ex: "page") - @param {String} outputFile: filename to write, relative to the output - @param {Object} context: context for the page - @return {Promise} -*/ -WebsiteOutput.prototype.renderAsString = function(tpl, context) { - // Calcul template name - var filename = this.templateName(tpl); - - context = _.extend(context, { - plugins: { - resources: this.resources - }, - - options: this.opts - }); - - // Create environment - var env = setupTemplateEnv(this, context); - - return Promise.nfcall(env.render.bind(env), filename, context); -}; - -/* - Render a template using nunjucks - Templates are stored in `_layouts` folders - - - @param {String} tpl: template name (ex: "page") - @param {String} outputFile: filename to write, relative to the output - @param {Object} context: context for the page - @return {Promise} -*/ -WebsiteOutput.prototype.render = function(tpl, outputFile, context) { - var that = this; - - // Calcul relative path to the root - var outputDirName = path.dirname(outputFile); - var basePath = location.normalize(path.relative(outputDirName, './')); - - // Setup complete context - context = _.extend(context, { - basePath: basePath, - - template: { - getJSContext: function() { - return { - page: _.omit(context.page, 'content'), - config: context.config, - file: context.file, - gitbook: context.gitbook, - basePath: basePath, - book: { - language: context.book.language - } - }; - } - } - }); - - return this.renderAsString(tpl, context) - .then(function(html) { - return that.writeFile( - outputFile, - html - ); - }); -}; - -// Return a complete name for a template -WebsiteOutput.prototype.templateName = function(name) { - return path.join(this.name, name+'.html'); -}; - -module.exports = WebsiteOutput; - - - diff --git a/lib/output2/website/templateEnv.js b/lib/output2/website/templateEnv.js deleted file mode 100644 index d385108..0000000 --- a/lib/output2/website/templateEnv.js +++ /dev/null @@ -1,95 +0,0 @@ -var _ = require('lodash'); -var nunjucks = require('nunjucks'); -var path = require('path'); -var fs = require('fs'); -var DoExtension = require('nunjucks-do')(nunjucks); - - -var location = require('../../utils/location'); -var defaultFilters = require('../../template/filters'); - -var ThemeLoader = require('./themeLoader'); - -// Directory for a theme with the templates -function templatesPath(dir) { - return path.join(dir, '_layouts'); -} - -/* - Create and setup at Nunjucks template environment - - @return {Nunjucks.Environment} -*/ -function setupTemplateEnv(output, context) { - context = _.defaults(context || {}, { - // Required by ThemeLoader - template: {} - }); - - var loader = new ThemeLoader( - _.map(output.searchPaths, templatesPath) - ); - var env = new nunjucks.Environment(loader); - - env.addExtension('DoExtension', new DoExtension()); - - // Add context as global - _.each(context, function(value, key) { - env.addGlobal(key, value); - }); - - // Add GitBook default filters - _.each(defaultFilters, function(fn, filter) { - env.addFilter(filter, fn); - }); - - // Translate using _i18n locales - env.addFilter('t', function t(s) { - return output.i18n.t(output.book.config.get('language'), s); - }); - - // Transform an absolute path into a relative path - // using this.ctx.page.path - env.addFilter('resolveFile', function resolveFile(href) { - return location.normalize(output.resolveForPage(context.file.path, href)); - }); - - // Test if a file exists - env.addFilter('fileExists', function fileExists(href) { - return fs.existsSync(output.resolve(href)); - }); - - // Transform a '.md' into a '.html' (README -> index) - env.addFilter('contentURL', function contentURL(s) { - return output.toURL(s); - }); - - // Get an article using its path - env.addFilter('getArticleByPath', function getArticleByPath(s) { - var article = output.book.summary.getArticle(s); - if (!article) return undefined; - - return article.getContext(); - }); - - // Relase path to an asset - env.addFilter('resolveAsset', function resolveAsset(href) { - href = path.join('gitbook', href); - - // Resolve for current file - if (context.file) { - href = output.resolveForPage(context.file.path, '/' + href); - } - - // Use assets from parent - if (output.book.isLanguageBook()) { - href = path.join('../', href); - } - - return location.normalize(href); - }); - - return env; -} - -module.exports = setupTemplateEnv; diff --git a/lib/output2/website/themeLoader.js b/lib/output2/website/themeLoader.js deleted file mode 100644 index 774a39e..0000000 --- a/lib/output2/website/themeLoader.js +++ /dev/null @@ -1,127 +0,0 @@ -var _ = require('lodash'); -var fs = require('fs'); -var path = require('path'); -var nunjucks = require('nunjucks'); - -/* - Nunjucks loader similar to FileSystemLoader, but avoid infinite looping -*/ - -/* - Return true if a filename is relative. -*/ -function isRelative(filename) { - return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); -} - -var ThemeLoader = nunjucks.Loader.extend({ - init: function(searchPaths) { - this.searchPaths = _.map(searchPaths, path.normalize); - }, - - /* - Read source of a resolved filepath - - @param {String} - @return {Object} - */ - getSource: function(fullpath) { - if (!fullpath) return null; - - fullpath = this.resolve(null, fullpath); - var templateName = this.getTemplateName(fullpath); - - if(!fullpath) { - return null; - } - - var src = fs.readFileSync(fullpath, 'utf-8'); - - src = '{% do %}var template = template || {}; template.stack = template.stack || []; template.stack.push(template.self); template.self = ' + JSON.stringify(templateName) + '{% enddo %}\n' + - src + - '\n{% do %}template.self = template.stack.pop();{% enddo %}'; - - return { - src: src, - path: fullpath, - noCache: true - }; - }, - - /* - Nunjucks calls "isRelative" to determine when to call "resolve". - We handle absolute paths ourselves in ".resolve" so we always return true - */ - isRelative: function() { - return true; - }, - - /* - Get original search path containing a template - - @param {String} filepath - @return {String} searchPath - */ - getSearchPath: function(filepath) { - return _.chain(this.searchPaths) - .sortBy(function(s) { - return -s.length; - }) - .find(function(basePath) { - return (filepath && filepath.indexOf(basePath) === 0); - }) - .value(); - }, - - /* - Get template name from a filepath - - @param {String} filepath - @return {String} name - */ - getTemplateName: function(filepath) { - var originalSearchPath = this.getSearchPath(filepath); - return originalSearchPath? path.relative(originalSearchPath, filepath) : null; - }, - - /* - Resolve a template from a current template - - @param {String|null} from - @param {String} to - @return {String|null} - */ - resolve: function(from, to) { - var searchPaths = this.searchPaths; - - // Relative template like "./test.html" - if (isRelative(to) && from) { - return path.resolve(path.dirname(from), to); - } - - // Determine in which search folder we currently are - var originalSearchPath = this.getSearchPath(from); - var originalFilename = this.getTemplateName(from); - - // If we are including same file from a different search path - // Slice the search paths to avoid including from previous ones - if (originalFilename == to) { - var currentIndex = searchPaths.indexOf(originalSearchPath); - searchPaths = searchPaths.slice(currentIndex + 1); - } - - // Absolute template to resolve in root folder - var resultFolder = _.find(searchPaths, function(basePath) { - var p = path.resolve(basePath, to); - - return ( - p.indexOf(basePath) === 0 - && fs.existsSync(p) - ); - }); - if (!resultFolder) return null; - return path.resolve(resultFolder, to); - } -}); - -module.exports = ThemeLoader; diff --git a/lib/page/html.js b/lib/page/html.js deleted file mode 100644 index e8d3a85..0000000 --- a/lib/page/html.js +++ /dev/null @@ -1,290 +0,0 @@ -var _ = require('lodash'); -var url = require('url'); -var cheerio = require('cheerio'); -var domSerializer = require('dom-serializer'); -var slug = require('github-slugid'); - -var Promise = require('../utils/promise'); -var location = require('../utils/location'); - -// Selector to ignore -var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6'; - -function HTMLPipeline(htmlString, opts) { - _.bindAll(this); - - this.opts = _.defaults(opts || {}, { - // Called once the description has been found - onDescription: function(description) { }, - - // Calcul new href for a relative link - onRelativeLink: _.identity, - - // Output an image - onImage: _.identity, - - // Syntax highlighting - onCodeBlock: _.identity, - - // Output a svg, if returns null the svg is kept inlined - onOutputSVG: _.constant(null), - - // Words to annotate - annotations: [], - - // When an annotation is applied - onAnnotation: function () { } - }); - - 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 - }); -} - -// Transform a query of elements in the page -HTMLPipeline.prototype._transform = function(query, fn) { - var 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)) { - // Preserve anchor - var parsed = url.parse(href); - var filename = this.opts.onRelativeLink(parsed.pathname); - - $a.attr('href', filename + (parsed.hash || '')); - } else { - // External links - $a.attr('target', '_blank'); - } - }); -}; - -// Normalize images -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.transformHeadings = function() { - var that = this; - - this.$('h1,h2,h3,h4,h5,h6').each(function() { - var $h = that.$(this); - - // Already has an ID? - if ($h.attr('id')) return; - $h.attr('id', slug($h.text())); - }); -}; - -// Outline SVG from the HML -HTMLPipeline.prototype.transformSvgs = function() { - var that = this; - - return this._transform('svg', function($svg) { - var content = [ - '<?xml version="1.0" encoding="UTF-8"?>', - renderDOM(that.$, $svg) - ].join('\n'); - - return Promise(that.opts.onOutputSVG(content)) - .then(function(filename) { - if (!filename) return; - - $svg.replaceWith(that.$('<img>').attr('src', filename)); - }); - }); -}; - -// Annotate the content -HTMLPipeline.prototype.applyAnnotations = function() { - var that = this; - - _.each(this.opts.annotations, function(annotation) { - var searchRegex = new RegExp( '\\b(' + pregQuote(annotation.name.toLowerCase()) + ')\\b' , 'gi' ); - - that.$('*').each(function() { - var $this = that.$(this); - - if ( - $this.is(ANNOTATION_IGNORE) || - $this.parents(ANNOTATION_IGNORE).length > 0 - ) return; - - replaceText(that.$, this, searchRegex, function(match) { - that.opts.onAnnotation(annotation); - - return '<a href="' + that.opts.onRelativeLink(annotation.href) + '" ' - + 'class="glossary-term" title="'+_.escape(annotation.description)+'">' - + match - + '</a>'; - }); - }); - }); -}; - -// Extract page description from html -// This can totally be improved -HTMLPipeline.prototype.extractDescription = function() { - var $ = this.$; - var $p = $('p').first(); - var $next = $p.nextUntil('h1,h2,h3,h4,h5,h6,pre,blockquote,ul,ol,div'); - - var description = $p.text().trim(); - - $next.each(function() { - description += ' ' + $(this).text().trim(); - }); - - // Truncate description - description = _.trunc(description, 300); - - this.opts.onDescription(description); -}; - -// Write content to the pipeline -HTMLPipeline.prototype.output = function() { - var that = this; - - return Promise() - .then(this.extractDescription) - .then(this.transformImages) - .then(this.transformHeadings) - .then(this.transformCodeBlocks) - .then(this.transformSvgs) - .then(this.applyAnnotations) - - // Transform of links should be applied after annotations - // because annotations are created as links - .then(this.transformLinks) - - .then(function() { - return renderDOM(that.$); - }); -}; - - -// Render a cheerio DOM as html -function renderDOM($, dom, options) { - if (!dom && $._root && $._root.children) { - dom = $._root.children; - } - options = options|| dom.options || $._options; - return domSerializer(dom, options); -} - -// Replace text in an element -function replaceText($, el, search, replace, text_only ) { - return $(el).each(function(){ - var node = this.firstChild, - val, - new_val, - - // Elements to be removed at the end. - remove = []; - - // Only continue if firstChild exists. - if ( node ) { - - // Loop over all childNodes. - while (node) { - - // Only process text nodes. - if ( node.nodeType === 3 ) { - - // The original node value. - val = node.nodeValue; - - // The new value. - new_val = val.replace( search, replace ); - - // Only replace text if the new value is actually different! - if ( new_val !== val ) { - - if ( !text_only && /</.test( new_val ) ) { - // The new value contains HTML, set it in a slower but far more - // robust way. - $(node).before( new_val ); - - // Don't remove the node yet, or the loop will lose its place. - remove.push( node ); - } else { - // The new value contains no HTML, so it can be set in this - // very fast, simple way. - node.nodeValue = new_val; - } - } - } - - node = node.nextSibling; - } - } - - // Time to remove those elements! - if (remove.length) $(remove).remove(); - }); -} - -function pregQuote( str ) { - return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); -} - -module.exports = HTMLPipeline; diff --git a/lib/page/index.js b/lib/page/index.js deleted file mode 100644 index f0d7f57..0000000 --- a/lib/page/index.js +++ /dev/null @@ -1,246 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var direction = require('direction'); -var fm = require('front-matter'); - -var error = require('../utils/error'); -var pathUtil = require('../utils/path'); -var location = require('../utils/location'); -var parsers = require('../parsers'); -var pluginCompatibility = require('../plugins/compatibility'); -var HTMLPipeline = require('./html'); - -/* -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.log = this.book.log; - - // Map of attributes from YAML frontmatter - // Description is also extracted by default from content - this.attributes = {}; - - // Current content - this.content = ''; - - // Relative path to the page - this.path = location.normalize(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.getByExt(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 pathUtil.setExtension(this.path, ext); -}; - -// 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)); -}; - -// Convert an absolute path (in the book) to a relative path from this page -Page.prototype.relative = function(name) { - // Convert /test.png -> test.png - name = location.toAbsolute(name, '', ''); - - return location.relative( - this.resolve('.') + '/', - this.book.resolve(name) - ); -}; - -// Return a page result of a relative page from this page -Page.prototype.followPage = function(filename) { - var absPath = this.resolveLocal(filename); - return this.book.getPage(absPath); -}; - -// Update content of the page -Page.prototype.update = function(content) { - this.content = content; -}; - -// Read the page as a string -Page.prototype.read = function() { - 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); -}; - -// Return templating context for this page -// 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; - - // Detect text direction in this page - var dir = this.book.config.get('direction'); - if (!dir) { - dir = direction(this.content); - if (dir == 'neutral') dir = null; - } - - return { - file: { - path: this.path, - mtime: this.mtime, - type: this.type - }, - page: _.extend({}, this.attributes, { - title: article? article.title : null, - next: next? next.getContext() : null, - previous: prev? prev.getContext() : null, - level: article? article.level : null, - depth: article? article.depth() : 0, - content: this.content, - dir: dir - }) - }; -}; - -// Return complete context for templating (page + book + summary + ...) -Page.prototype.getOutputContext = function(output) { - return _.extend({}, this.getContext(), output.getContext()); -}; - -// Parse the page and return its content -Page.prototype.toHTML = function(output) { - var that = this; - - this.log.debug.ln('start parsing file', this.path); - - // Call a hook in the output - // using an utility to "keep" compatibility with gitbook 2 - function hook(name) { - return pluginCompatibility.pageHook(that, function(ctx) { - return output.plugins.hook(name, ctx); - }) - .then(function(result) { - if(_.isString(result)) that.update(result); - }); - } - - return this.read() - - // Parse yaml front matter - .then(function() { - var parsed = fm(that.content); - - // Extract attributes - that.attributes = parsed.attributes; - - // Keep only the body - that.update(parsed.body); - }) - - .then(function() { - return hook('page:before'); - }) - - // Pre-process page with parser - .then(function() { - return that.parser.page.prepare(that.content) - .then(that.update); - }) - - // Render template - .then(function() { - return output.template.render(that.content, that.getOutputContext(output), { - path: that.path - }) - .then(that.update); - }) - - // Render markup using the parser - .then(function() { - return that.parser.page(that.content) - .then(function(out) { - that.update(out.content); - }); - }) - - // Post process templating - .then(function() { - return output.template.postProcess(that.content) - .then(that.update); - }) - - // Normalize HTML output - .then(function() { - var pipelineOpts = { - onRelativeLink: _.partial(output.onRelativeLink, that), - onImage: _.partial(output.onOutputImage, that), - onOutputSVG: _.partial(output.onOutputSVG, that), - - // Use 'code' template block - onCodeBlock: function(source, lang) { - return output.template.applyBlock('code', { - body: source, - kwargs: { - language: lang - } - }); - }, - - // Extract description from page's content if no frontmatter - onDescription: function(description) { - if (that.attributes.description) return; - that.attributes.description = description; - }, - - // Convert glossary entries to annotations - annotations: that.book.glossary.annotations() - }; - var pipeline = new HTMLPipeline(that.content, pipelineOpts); - - return pipeline.output() - .then(that.update); - }) - - .then(function() { - return hook('page'); - }) - - // Return content itself - .then(function() { - return that.content; - }); -}; - - -module.exports = Page; diff --git a/lib/parse/parsePage.js b/lib/parse/parsePage.js index 75bcf61..1d515d6 100644 --- a/lib/parse/parsePage.js +++ b/lib/parse/parsePage.js @@ -1,4 +1,6 @@ +var Immutable = require('immutable'); var fm = require('front-matter'); +var direction = require('direction'); /** Parse a page, read its content and parse the YAMl header @@ -15,10 +17,11 @@ function parsePage(book, page) { .then(function(content) { var parsed = fm(content); - page = page.set('content', parsed.body); - page = page.set('attributes', parsed.attributes); - - return page; + return page.merge({ + content: parsed.body, + attributes: Immutable.fromJS(parsed.attributes), + dir: direction(parsed.body) + }); }); } diff --git a/lib/plugins2/compatibility.js b/lib/plugins2/compatibility.js deleted file mode 100644 index 77f4be2..0000000 --- a/lib/plugins2/compatibility.js +++ /dev/null @@ -1,61 +0,0 @@ -var _ = require('lodash'); -var error = require('../utils/error'); - -/* - Return the context for a plugin. - It tries to keep compatibilities with GitBook v2 -*/ -function pluginCtx(plugin) { - var book = plugin.book; - var ctx = book; - - return ctx; -} - -/* - Call a function "fn" with a context of page similar to the one in GitBook v2 - - @params {Page} - @returns {String|undefined} new content of the page -*/ -function pageHook(page, fn) { - // Get page context - var ctx = page.getContext().page; - - // Add other informations - ctx.type = page.type; - ctx.rawPath = page.rawPath; - ctx.path = page.path; - - // Deprecate sections - error.deprecateField(ctx, 'sections', [ - { content: ctx.content, type: 'normal' } - ], '"sections" property is deprecated, use page.content instead'); - - // Keep reference of original content for compatibility - var originalContent = ctx.content; - - return fn(ctx) - .then(function(result) { - // No returned value - // Existing content will be used - if (!result) return undefined; - - // GitBook 3 - // Use returned page.content if different from original content - if (result.content != originalContent) { - return result.content; - } - - // GitBook 2 compatibility - // Finally, use page.sections - if (result.sections) { - return _.pluck(result.sections, 'content').join('\n'); - } - }); -} - -module.exports = { - pluginCtx: pluginCtx, - pageHook: pageHook -}; diff --git a/lib/plugins2/index.js b/lib/plugins2/index.js deleted file mode 100644 index c6f1686..0000000 --- a/lib/plugins2/index.js +++ /dev/null @@ -1,188 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); - -var Promise = require('../utils/promise'); -var fs = require('../utils/fs'); -var BookPlugin = require('./plugin'); -var registry = require('./registry'); -var pluginsConfig = require('../config/plugins'); - -/* -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.log = this.book.log; - this.plugins = []; - - _.bindAll(this); -} - -// Returns the list of plugins -PluginsManager.prototype.list = function() { - return this.plugins; -}; - -// 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, { - id: name - }); -}; - -// Load a plugin (could be a BookPlugin or {name,path}) -PluginsManager.prototype.load = function(plugin) { - var that = this; - - if (_.isArray(plugin)) { - return Promise.serie(plugin, that.load); - } - - return Promise() - - // Initiate and load the plugin - .then(function() { - if (!(plugin instanceof BookPlugin)) { - plugin = new BookPlugin(that.book, plugin.name, plugin.path); - } - - 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); - }) - - // Setup the plugin - .then(this._setup); -}; - -// Load all plugins from the book's configuration -PluginsManager.prototype.loadAll = function() { - var that = this; - var pluginNames = _.pluck(this.book.config.get('plugins'), 'name'); - - return registry.list(this.book) - .then(function(plugins) { - // Filter out plugins not listed of first level - // (aka pre-installed plugins) - plugins = _.filter(plugins, function(plugin) { - return ( - plugin.depth > 1 || - _.contains(pluginNames, plugin.name) - ); - }); - - // Sort plugins to match list in book.json - plugins.sort(function(a, b){ - return pluginNames.indexOf(a.name) < pluginNames.indexOf(b.name) ? -1 : 1; - }); - - // Log state - that.log.info.ln(_.size(plugins) + ' are installed'); - if (_.size(pluginNames) != _.size(plugins)) that.log.info.ln(_.size(pluginNames) + ' explicitly listed'); - - // Verify that all plugins are present - var notInstalled = _.filter(pluginNames, function(name) { - return !_.find(plugins, { name: name }); - }); - - if (_.size(notInstalled) > 0) { - throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.'); - } - - // Load plugins - return that.load(plugins); - }); -}; - -// 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() { - var that = this; - var plugins = _.filter(this.book.config.get('plugins'), function(plugin) { - return !pluginsConfig.isDefaultPlugin(plugin.name); - }); - - 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(that.book, plugin.name, plugin.version); - }) - .thenResolve(plugins.length); -}; - -// Call a hook on all plugins to transform an input -PluginsManager.prototype.hook = function(name, input) { - return Promise.reduce(this.plugins, function(current, plugin) { - return plugin.hook(name, current); - }, input); -}; - -// Extract all resources for a namespace -PluginsManager.prototype.getResources = function(namespace) { - return Promise.reduce(this.plugins, function(out, plugin) { - return plugin.getResources(namespace) - .then(function(pluginResources) { - _.each(BookPlugin.RESOURCES, function(resourceType) { - out[resourceType] = (out[resourceType] || []).concat(pluginResources[resourceType] || []); - }); - - return out; - }); - }, {}); -}; - -// Copy all resources for a plugin -PluginsManager.prototype.copyResources = function(namespace, outputRoot) { - return Promise.serie(this.plugins, function(plugin) { - return plugin.getResources(namespace) - .then(function(resources) { - if (!resources.assets) return; - - var input = path.resolve(plugin.root, resources.assets); - var output = path.resolve(outputRoot, plugin.npmId); - - return fs.copyDir(input, output); - }); - }); -}; - -// Get all filters and blocks -PluginsManager.prototype.getFilters = function() { - return _.reduce(this.plugins, function(out, plugin) { - var filters = plugin.getFilters(); - - return _.extend(out, filters); - }, {}); -}; -PluginsManager.prototype.getBlocks = function() { - return _.reduce(this.plugins, function(out, plugin) { - var blocks = plugin.getBlocks(); - - return _.extend(out, blocks); - }, {}); -}; - -module.exports = PluginsManager; diff --git a/lib/plugins2/plugin.js b/lib/plugins2/plugin.js deleted file mode 100644 index d1c00d8..0000000 --- a/lib/plugins2/plugin.js +++ /dev/null @@ -1,288 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var url = require('url'); -var resolve = require('resolve'); -var mergeDefaults = require('merge-defaults'); -var jsonschema = require('jsonschema'); -var jsonSchemaDefaults = require('json-schema-defaults'); - -var Promise = require('../utils/promise'); -var error = require('../utils/error'); -var gitbook = require('../gitbook'); -var registry = require('./registry'); -var compatibility = require('./compatibility'); - -var HOOKS = [ - 'init', 'finish', 'finish:before', 'config', 'page', 'page:before' -]; - -var RESOURCES = ['js', 'css']; - -// Return true if an error is a "module not found" -// Wait on https://github.com/substack/node-resolve/pull/81 to be merged -function isModuleNotFound(err) { - return err.message.indexOf('Cannot find module') >= 0; -} - -function BookPlugin(book, pluginId, pluginFolder) { - this.book = book; - this.log = this.book.log.prefix(pluginId); - - - this.id = pluginId; - this.npmId = registry.npmId(pluginId); - this.root = pluginFolder; - - this.packageInfos = undefined; - this.content = undefined; - - // Cache for resources - this._resources = {}; - - _.bindAll(this); -} - -// Return true if plugin has been loaded correctly -BookPlugin.prototype.isLoaded = function() { - return Boolean(this.packageInfos && this.content); -}; - -// Bind a function to the plugin's context -BookPlugin.prototype.bind = function(fn) { - return fn.bind(compatibility.pluginCtx(this)); -}; - -// Load this plugin from its root folder -BookPlugin.prototype.load = function(folder) { - var that = this; - - if (this.isLoaded()) { - return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded')); - } - - // Try loading plugins from different location - var p = Promise() - .then(function() { - // Locate plugin and load pacjage.json - try { - var res = resolve.sync('./package.json', { basedir: that.root }); - - that.root = path.dirname(res); - that.packageInfos = require(res); - } catch (err) { - if (!isModuleNotFound(err)) throw err; - - that.packageInfos = undefined; - that.content = undefined; - - return; - } - - // Load plugin JS content - try { - that.content = require(that.root); - } catch(err) { - // It's no big deal if the plugin doesn't have an "index.js" - // (For example: themes) - if (isModuleNotFound(err)) { - that.content = {}; - } else { - throw new error.PluginError(err, { - plugin: that.id - }); - } - } - }) - - .then(that.validate) - - // Validate the configuration and update it - .then(function() { - var config = that.book.config.get(that.getConfigKey(), {}); - return that.validateConfig(config); - }) - .then(function(config) { - that.book.config.set(that.getConfigKey(), config); - }); - - this.log.info('loading plugin "' + this.id + '"... '); - return this.log.info.promise(p); -}; - -// Verify the definition of a plugin -// Also verify that the plugin accepts the current gitbook version -// This method throws erros if plugin is invalid -BookPlugin.prototype.validate = function() { - var isValid = ( - this.isLoaded() && - this.packageInfos && - this.packageInfos.name && - this.packageInfos.engines && - this.packageInfos.engines.gitbook - ); - - if (!isValid) { - throw new Error('Error loading plugin "' + this.id + '" at "' + this.root + '"'); - } - - if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) { - throw new Error('GitBook doesn\'t satisfy the requirements of this plugin: '+this.packageInfos.engines.gitbook); - } -}; - -// Normalize, validate configuration for this plugin using its schema -// Throw an error when shcema is not respected -BookPlugin.prototype.validateConfig = function(config) { - var that = this; - - return Promise() - .then(function() { - var schema = that.packageInfos.gitbook || {}; - if (!schema) return config; - - // Normalize schema - schema.id = '/'+that.getConfigKey(); - schema.type = 'object'; - - // Validate and throw if invalid - var v = new jsonschema.Validator(); - var result = v.validate(config, schema, { - propertyName: that.getConfigKey() - }); - - // Throw error - if (result.errors.length > 0) { - throw new error.ConfigurationError(new Error(result.errors[0].stack)); - } - - // Insert default values - var defaults = jsonSchemaDefaults(schema); - return mergeDefaults(config, defaults); - }); -}; - -// Return key for configuration -BookPlugin.prototype.getConfigKey = function() { - return 'pluginsConfig.'+this.id; -}; - -// Call a hook and returns its result -BookPlugin.prototype.hook = function(name, input) { - var that = this; - var hookFunc = this.content.hooks? this.content.hooks[name] : null; - input = input || {}; - - if (!hookFunc) return Promise(input); - - this.book.log.debug.ln('call hook "' + name + '" for plugin "' + this.id + '"'); - if (!_.contains(HOOKS, name)) { - this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions'); - } - - return Promise() - .then(function() { - return that.bind(hookFunc)(input); - }); -}; - -// Return resources without normalization -BookPlugin.prototype._getResources = function(base) { - var that = this; - - return Promise() - .then(function() { - if (that._resources[base]) return that._resources[base]; - - var book = that.content[base]; - - // Compatibility with version 1.x.x - if (base == 'website') book = book || that.content.book; - - // Nothing specified, fallback to default - if (!book) { - return Promise({}); - } - - // Dynamic function - if(typeof book === 'function') { - // Call giving it the context of our book - return that.bind(book)(); - } - - // Plain data object - return book; - }) - - .then(function(resources) { - that._resources[base] = resources; - return _.cloneDeep(resources); - }); -}; - -// Normalize a specific resource -BookPlugin.prototype.normalizeResource = function(resource) { - // Parse the resource path - var parsed = url.parse(resource); - - // This is a remote resource - // so we will simply link to using it's URL - if (parsed.protocol) { - return { - 'url': resource - }; - } - - // This will be copied over from disk - // and shipped with the book's build - return { 'path': this.npmId+'/'+resource }; -}; - - -// Normalize resources and return them -BookPlugin.prototype.getResources = function(base) { - var that = this; - - return this._getResources(base) - .then(function(resources) { - _.each(RESOURCES, function(resourceType) { - resources[resourceType] = _.map(resources[resourceType] || [], that.normalizeResource); - }); - - return resources; - }); -}; - -// Normalize filters and return them -BookPlugin.prototype.getFilters = function() { - var that = this; - - return _.mapValues(this.content.filters || {}, function(fn, filter) { - return function() { - var ctx = _.extend(compatibility.pluginCtx(that), this); - - return fn.apply(ctx, arguments); - }; - }); -}; - -// Normalize blocks and return them -BookPlugin.prototype.getBlocks = function() { - var that = this; - - return _.mapValues(this.content.blocks || {}, function(block, blockName) { - block = _.isFunction(block)? { process: block } : block; - - var fn = block.process; - block.process = function() { - var ctx = _.extend(compatibility.pluginCtx(that), this); - - return fn.apply(ctx, arguments); - }; - - return block; - }); -}; - -module.exports = BookPlugin; -module.exports.RESOURCES = RESOURCES; - diff --git a/lib/plugins2/registry.js b/lib/plugins2/registry.js deleted file mode 100644 index fe9406d..0000000 --- a/lib/plugins2/registry.js +++ /dev/null @@ -1,172 +0,0 @@ -var npm = require('npm'); -var npmi = require('npmi'); -var path = require('path'); -var semver = require('semver'); -var _ = require('lodash'); -var readInstalled = require('read-installed'); - -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(''); -} - -// 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 && 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, 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 + '" installed with success'); - }); -} - -// List all packages installed inside a folder -// Returns an ordered list of plugins -function listInstalled(folder) { - var options = { - dev: false, - log: function() {}, - depth: 4 - }; - var results = []; - - function onPackage(pkg, isRoot) { - if (!validateId(pkg.name)){ - if (!isRoot) return; - } else { - results.push({ - name: pluginId(pkg.name), - version: pkg.version, - path: pkg.realPath, - depth: pkg.depth - }); - } - - _.each(pkg.dependencies, function(dep) { - onPackage(dep); - }); - } - - return Promise.nfcall(readInstalled, folder, options) - .then(function(data) { - onPackage(data, true); - return _.uniq(results, 'name'); - }); -} - -// List installed plugins for a book (defaults and installed) -function listPlugins(book) { - return Promise.all([ - listInstalled(path.resolve(__dirname, '../..')), - listInstalled(book.root), - book.originalRoot? listInstalled(book.originalRoot) : Promise([]), - book.isLanguageBook()? listInstalled(book.parent.root) : Promise([]) - ]) - .spread(function() { - var args = _.toArray(arguments); - - var results = _.reduce(args, function(out, a) { - return out.concat(a); - }, []); - - return _.uniq(results, 'name'); - }); -} - -module.exports = { - npmId: npmId, - pluginId: pluginId, - validateId: validateId, - - resolve: resolveVersion, - link: linkPlugin, - install: installPlugin, - list: listPlugins, - listInstalled: listInstalled -}; diff --git a/lib/template/blocks.js b/lib/template/blocks.js deleted file mode 100644 index 5dfb0c8..0000000 --- a/lib/template/blocks.js +++ /dev/null @@ -1,36 +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 replaced by plugins - code: function(blk) { - return { - html: false, - body: blk.body - }; - }, - - // Render some markdown to HTML - markdown: function(blk) { - return this.book.renderInline('markdown', blk.body) - .then(function(out) { - return { body: out }; - }); - }, - asciidoc: function(blk) { - return this.book.renderInline('asciidoc', blk.body) - .then(function(out) { - return { body: out }; - }); - }, - markup: function(blk) { - return this.book.renderInline(this.ctx.file.type, blk.body) - .then(function(out) { - return { body: out }; - }); - } -}; diff --git a/lib/template/filters.js b/lib/template/filters.js deleted file mode 100644 index ac68b82..0000000 --- a/lib/template/filters.js +++ /dev/null @@ -1,15 +0,0 @@ -var moment = require('moment'); - - -module.exports = { - // Format a date - // ex: 'MMMM Do YYYY, h:mm:ss a - date: function(time, format) { - return moment(time).format(format); - }, - - // Relative Time - dateFromNow: function(time) { - return moment(time).fromNow(); - } -}; diff --git a/lib/template/index.js b/lib/template/index.js deleted file mode 100644 index ae11bc9..0000000 --- a/lib/template/index.js +++ /dev/null @@ -1,552 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var nunjucks = require('nunjucks'); -var escapeStringRegexp = require('escape-string-regexp'); - -var Promise = require('../utils/promise'); -var error = require('../utils/error'); -var parsers = require('../parsers'); -var defaultBlocks = require('./blocks'); -var defaultFilters = require('./filters'); -var Loader = require('./loader'); - -var NODE_ENDARGS = '%%endargs%%'; - -// 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; -} - -// Extract kwargs from an arguments array -function extractKwargs(args) { - var last = _.last(args); - return (_.isObject(last) && last.__keywords)? args.pop() : {}; -} - -function TemplateEngine(output) { - this.output = output; - this.book = output.book; - this.log = this.book.log; - - // Create file loader - this.loader = new Loader(this); - - // Create nunjucks instance - 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 and filters - this.addBlocks(defaultBlocks); - this.addFilters(defaultFilters); - - // Build context for this book with depreacted fields - this.ctx = { - template: this, - book: this.book, - output: this.output - }; - error.deprecateField(this.ctx, 'generator', this.output.name, '"generator" property is deprecated, use "output.generator" instead'); -} - -/* - Bind a function to a context - Filters and blocks are binded to this context. - - @param {Function} - @param {Function} -*/ -TemplateEngine.prototype.bindContext = function(func) { - var that = this; - - return function() { - var ctx = _.extend({ - ctx: this.ctx - }, that.ctx); - - return func.apply(ctx, arguments); - }; -}; - -/* - Interpolate a string content to replace shortcuts according to the filetype. - - @param {String} filepath - @param {String} source - @param {String} -*/ -TemplateEngine.prototype.interpolate = function(filepath, source) { - var parser = parsers.getByExt(path.extname(filepath)); - var type = parser? parser.name : null; - - return this.applyShortcuts(type, source); -}; - -/* - Add a new custom filter, it bind to the right context - - @param {String} - @param {Function} -*/ -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 - - @param {Map<String:Function>} -*/ -TemplateEngine.prototype.addFilters = function(filters) { - _.each(filters, function(filter, name) { - this.addFilter(name, filter); - }, this); -}; - -/* - Return true if a block is defined - - @param {String} -*/ -TemplateEngine.prototype.hasBlock = function(name) { - return this.env.hasExtension(blockExtName(name)); -}; - -/* - Remove/Disable a block - - @param {String} -*/ -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 - - @param {String} name - @param {BlockDescriptor|Function} block - @param {Function} block.process: function to be called to render the block - @param {String} block.end: name of the end tag of this block (default to "end<name>") - @param {Array<String>} block.blocks: list of inner blocks to parse - @param {Array<Shortcut>} block.shortcuts: list of shortcuts to parse this block -*/ -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, - blocks: [] - }); - - extName = blockExtName(name); - - if (!block.process) { - throw new Error('Invalid block "' + name + '", it should have a "process" method'); - } - - 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 lastBlockName = null; - var lastBlockArgs = null; - var allBlocks = block.blocks.concat([block.end]); - - // Parse first block - var tok = parser.nextToken(); - lastBlockArgs = parser.parseSignature(null, true); - parser.advanceAfterBlockEnd(tok.value); - - var args = new nodes.NodeList(); - var bodies = []; - var blockNamesNode = new nodes.Array(tok.lineno, tok.colno); - var blockArgCounts = new nodes.Array(tok.lineno, tok.colno); - - // Parse while we found "end<block>" - do { - // Read body - var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks); - - // Handle body with previous block name and args - blockNamesNode.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockName)); - blockArgCounts.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockArgs.children.length)); - bodies.push(currentBody); - - // Append arguments of this block as arguments of the run function - _.each(lastBlockArgs.children, function(child) { - args.addChild(child); - }); - - // Read new block - lastBlockName = parser.nextToken().value; - - // Parse signature and move to the end of the block - if (lastBlockName != block.end) { - lastBlockArgs = parser.parseSignature(null, true); - } - - parser.advanceAfterBlockEnd(lastBlockName); - } while (lastBlockName != block.end); - - args.addChild(blockNamesNode); - args.addChild(blockArgCounts); - args.addChild(new nodes.Literal(args.lineno, args.colno, NODE_ENDARGS)); - - return new nodes.CallExtensionAsync(this, 'run', args, bodies); - }; - - this.run = function(context) { - var fnArgs = Array.prototype.slice.call(arguments, 1); - - var args; - var blocks = []; - var bodies = []; - var blockNames; - var blockArgCounts; - var callback; - - // Extract callback - callback = fnArgs.pop(); - - // Detect end of arguments - var endArgIndex = fnArgs.indexOf(NODE_ENDARGS); - - // Extract arguments and bodies - args = fnArgs.slice(0, endArgIndex); - bodies = fnArgs.slice(endArgIndex + 1); - - // Extract block counts - blockArgCounts = args.pop(); - blockNames = args.pop(); - - // Recreate list of blocks - _.each(blockNames, function(name, i) { - var countArgs = blockArgCounts[i]; - var blockBody = bodies.shift(); - - var blockArgs = countArgs > 0? args.slice(0, countArgs) : []; - args = args.slice(countArgs); - var blockKwargs = extractKwargs(blockArgs); - - blocks.push({ - name: name, - body: blockBody(), - args: blockArgs, - kwargs: blockKwargs - }); - }); - - var mainBlock = blocks.shift(); - mainBlock.blocks = blocks; - - Promise() - .then(function() { - return that.applyBlock(name, mainBlock, 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 - - @param {Array<BlockDescriptor>} -*/ -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) - - - @param {String} name: name of the block type to apply - @param {Block} blk: content of the block - @param {Object} ctx: context of execution of the block - @return {Block|Promise<Block>} -*/ -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.isPromiseAlike(r)) return Promise(r).then(normBlockResult); - else return normBlockResult(r); -}; - -/* - Process the result of block in a context. It returns the content to append to the output. - It can return an "anchor" that will be replaced by "replaceBlocks" in "postProcess" - - @param {Block} - @return {String} -*/ -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+'%-}}'; -}; - -/* - Render a string (without post processing) - - @param {String} content: template's content to render - @param {Object} context - @param {Object} options - @param {String} options.path: pathname to the template - @return {Promise<String>} -*/ -TemplateEngine.prototype.render = function(content, context, options) { - options = _.defaults(options || {}, { - path: null - }); - var filename = options.path; - - // Setup path and type - if (options.path) { - options.path = this.book.resolve(options.path); - } - - // 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.TemplateError(err, { - filename: filename || '<inline>' - }); - }); -}; - -/* - Render a string (with post processing) - - @param {String} content: template's content to render - @param {Object} context - @param {Object} options - @return {Promise<String>} -*/ -TemplateEngine.prototype.renderString = function(content, context, options) { - return this.render(content, context, options) - .then(this.postProcess); -}; - -/* - Apply a shortcut of block to a template - - @param {String} content - @param {Shortcut} shortcut - @return {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 shortcut of blocks to a template - - @param {String} type: type of template ("markdown", "asciidoc") - @param {String} content - @return {String} -*/ -TemplateEngine.prototype.applyShortcuts = function(type, content) { - return _.chain(this.shortcuts) - .filter(function(shortcut) { - return _.contains(shortcut.parsers, type); - }) - .reduce(this.applyShortcut, content) - .value(); -}; - -/* - Replace position markers of blocks by body after processing - This is done to avoid that markdown/asciidoc processer parse the block content - - @param {String} content - @return {String} -*/ -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; - }); -}; - - - -/* - Post process templating result: remplace block's anchors and apply "post" - - @param {String} content - @return {Promise<String>} -*/ -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/template/loader.js b/lib/template/loader.js deleted file mode 100644 index 23d179a..0000000 --- a/lib/template/loader.js +++ /dev/null @@ -1,42 +0,0 @@ -var nunjucks = require('nunjucks'); -var location = require('../utils/location'); - -/* -Simple nunjucks loader which is passing the reponsability to the Output -*/ - -var Loader = nunjucks.Loader.extend({ - async: true, - - 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); - }, - - resolve: function(from, to) { - return this.output.onResolveTemplate(from, to); - }, - - // Handle all files as relative, so that nunjucks pass responsability to 'resolve' - isRelative: function(filename) { - return location.isRelative(filename); - } -}); - -module.exports = Loader; |