├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── CHANGES.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bin ├── build.js ├── gitbook.js ├── platform.js ├── server.js └── utils.js ├── bower.json ├── gitbook.js ├── gitbook.min.js ├── lib ├── generate │ ├── config.js │ ├── ebook │ │ └── index.js │ ├── fs.js │ ├── generator.js │ ├── index.js │ ├── init.js │ ├── json │ │ └── index.js │ ├── page │ │ └── index.js │ ├── plugin.js │ ├── site │ │ ├── glossary_indexer.js │ │ ├── index.js │ │ └── search_indexer.js │ └── template.js ├── index.js ├── parse │ ├── glossary.js │ ├── include.js │ ├── includer.js │ ├── index.js │ ├── is_exercise.js │ ├── is_quiz.js │ ├── langs.js │ ├── lex.js │ ├── navigation.js │ ├── page.js │ ├── progress.js │ ├── readme.js │ ├── renderer.js │ └── summary.js └── utils │ ├── index.js │ ├── lang.js │ ├── links.js │ └── string.js ├── package.json ├── preview.png ├── test ├── bin │ ├── lex.js │ └── summary.js ├── fixtures │ ├── ALTERNATIVE_SUMMARY.md │ ├── FALSE_QUIZ.md │ ├── GITHUB_LINKS.md │ ├── GLOSSARY.md │ ├── HR_PAGE.md │ ├── IMAGES.md │ ├── INCLUDES.md │ ├── MARKDOWN_LINKS.md │ ├── PAGE.md │ ├── QUIZ_PAGE.md │ ├── README.md │ ├── SECTIONS.md │ ├── SUMMARY.md │ ├── SUMMARY_WHITESPACE.md │ ├── book1 │ │ ├── README.md │ │ ├── SUMMARY.md │ │ └── test.md │ ├── book2 │ │ └── README.md │ └── included.c ├── generate.js ├── glossary.js ├── includes.js ├── navigation.js ├── page.js ├── plugin.js ├── readme.js ├── sections.js └── summary.js └── theme ├── assets ├── app.js ├── fonts │ ├── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── merriweather │ │ ├── 250.woff │ │ ├── 250i.woff │ │ ├── 400.woff │ │ ├── 400i.woff │ │ ├── 700.woff │ │ ├── 700i.woff │ │ ├── 900.woff │ │ └── 900i.woff │ └── opensans │ │ ├── 300.woff │ │ ├── 300i.woff │ │ ├── 400.woff │ │ ├── 400i.woff │ │ ├── 600.woff │ │ ├── 600i.woff │ │ ├── 700.woff │ │ └── 700i.woff ├── images │ ├── apple-touch-icon-precomposed-152.png │ └── favicon.ico ├── print.css └── style.css ├── javascript ├── core │ ├── events.js │ ├── font-settings.js │ ├── glossary.js │ ├── keyboard.js │ ├── loading.js │ ├── navigation.js │ ├── progress.js │ ├── search.js │ ├── sidebar.js │ └── state.js ├── gitbook.js └── utils │ ├── dropdown.js │ ├── highlight.js │ ├── platform.js │ ├── qrcode.js │ ├── sharing.js │ ├── storage.js │ └── url.js ├── stylesheets ├── base │ ├── markdown.less │ ├── normalize.less │ └── preboot.less ├── ebook.less ├── ebook │ ├── highlight.less │ └── variables.less ├── mixins.less ├── website.less └── website │ ├── alerts.less │ ├── body.less │ ├── buttons.less │ ├── dropdown.less │ ├── font-settings.less │ ├── fonts.less │ ├── glossary.less │ ├── header.less │ ├── highlight │ ├── night.less │ ├── sepia.less │ └── white.less │ ├── languages.less │ ├── markdown.less │ ├── navigation.less │ ├── search-highlight.less │ ├── summary.less │ └── variables.less └── templates ├── ebook ├── glossary.html ├── includes │ ├── exercise.html │ └── quiz.html ├── layout.html ├── page.html └── summary.html └── website ├── glossary.html ├── includes ├── exercise.html ├── font-settings.html ├── header.html ├── quiz.html └── summary.html ├── langs.html ├── layout.html └── page.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | theme/javascript/vendors 28 | theme/vendors 29 | 30 | # vim swapfile 31 | *.swp 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Also see https://github.com/GitbookIO/gitbook/graphs/contributors. 5 | Names below are ordered by first contribution. 6 | 7 | Author 8 | ------ 9 | 10 | - Samy Pessé 11 | - Aaron O'Mullan 12 | 13 | 14 | Contributors 15 | ------------ 16 | 17 | - Nijiko Yonskai 18 | - Herman Starikov 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | ## 1.5.0 4 | - Fix `serve` command, broken by `1.4.2` 5 | - Add nicer `dark` theme :) 6 | 7 | ## 1.4.2 8 | - Force `process.exit` after builds, to prevent (possibly) lingering plugins 9 | 10 | ## 1.4.1 11 | - Fix command 'install' without arguments 12 | 13 | ## 1.4.0 14 | - Add command `gitbook install` to install plugins from book.json 15 | - `package.json` is no longer necessary 16 | 17 | ## 1.3.4 18 | - Add glossary to ebooks 19 | - Fix autocover with new hook "finish:before" 20 | - Add X-UA-Compatible meta tag for IE 21 | 22 | ## 1.3.3 23 | - Fix parsing of lexed content using the client library 24 | 25 | ## 1.3.2 26 | - ePub files are now passing validation from epubcheck 27 | - Fix replacement of multiple glossary terms in a single sentence 28 | - Fix on windows deep relative links 29 | - Fix search indexer 30 | 31 | ## 1.3.1 32 | - Fix error with links in markdown 33 | 34 | ## 1.3.0 35 | - Bundle gitbook parsing library as a client side library in `gitbook.js` and `gitbook.min.js` 36 | 37 | ## 1.2.0 38 | - Improvements on ebook generation 39 | - Fix incorrect follow of links in ebook generation 40 | - Move Table of Contents at the beginning of the ebook 41 | - Update to last highlight.js (includes Swift) 42 | - Includes of templates and variables (from book.json) 43 | 44 | ## 1.1.1 45 | - Rewrite quiz logic to be more robust 46 | - Improve integration of glossary 47 | - Improve generation of ebook by using a multiple HTML pages input source 48 | - Fix incorrect page breaks after h1 and h2 divs 49 | - New options to set header and footer in PDF generation 50 | 51 | ## 1.1.0 52 | - Plugins can now extend the ebook generation (pdf, epub, mobi) 53 | - Update `kramed` to version 0.4.3 54 | 55 | ## 1.0.3 56 | - Update `mathjax` plugin and MathJAx to version 2.4 57 | - Update `highlight.js` to 8.2.0 58 | 59 | ## 1.0.2 60 | - Update `mathjax` plugin, fixes issues with inline math rendering (no longer wanted) 61 | 62 | ## 1.0.1 63 | - New inline math convention (kramdown's), using `$$` rather than `$` as delimiters 64 | - Fix instapaper sharing 65 | - The `exercises` & `quizzes` plugins are now by default 66 | 67 | ## 1.0.0 68 | - New design 69 | - Support for glossary 70 | - Support for sharing to instapaper 71 | - Support for footnotes 72 | 73 | ## 0.7.1 74 | - Update `fs-extra` to `0.10.0` (fixes potential race conditions) 75 | 76 | ## 0.7.0 77 | - Add page break in ebook (pdf, epub, mobi) between chapters/articles 78 | - Start using kramed instead of marked 79 | - Fix display of inline math 80 | - Switch to graceful-fs to fix EMFILE errors 81 | - Add sharing to weibo.com 82 | 83 | ## 0.6.2 84 | - Support generating a plugin's book info dynamically 85 | - Improve navigation on dark theme 86 | - Improve path normalization when parsing SUMMARY.md 87 | 88 | ## 0.6.0 89 | - Generate header id the same as github 90 | - Custom links can be added at top of sidebar 91 | - Summary can now be transformed by plugins 92 | - Support importing code snippets 93 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var path = require("path"); 3 | 4 | // Load NPM tasks 5 | grunt.loadNpmTasks('grunt-contrib-copy'); 6 | grunt.loadNpmTasks('grunt-contrib-less'); 7 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 8 | grunt.loadNpmTasks("grunt-bower-install-simple"); 9 | grunt.loadNpmTasks('grunt-browserify'); 10 | grunt.loadNpmTasks('grunt-contrib-uglify'); 11 | 12 | // Init GRUNT configuraton 13 | grunt.initConfig({ 14 | pkg: grunt.file.readJSON('package.json'), 15 | 'bower-install-simple': { 16 | options: { 17 | color: true, 18 | production: false, 19 | directory: "theme/vendors" 20 | } 21 | }, 22 | less: { 23 | development: { 24 | options: { 25 | compress: true, 26 | yuicompress: true, 27 | optimization: 2 28 | }, 29 | files: { 30 | "theme/assets/style.css": "theme/stylesheets/website.less", 31 | "theme/assets/print.css": "theme/stylesheets/ebook.less" 32 | } 33 | } 34 | }, 35 | requirejs: { 36 | compile: { 37 | options: { 38 | name: "gitbook", 39 | baseUrl: "theme/javascript/", 40 | out: "theme/assets/app.js", 41 | preserveLicenseComments: false, 42 | optimize: "uglify", 43 | include: ["requireLib"], 44 | paths: { 45 | "jQuery": '../vendors/jquery/dist/jquery', 46 | "lodash": '../vendors/lodash/dist/lodash', 47 | "requireLib": '../vendors/requirejs/require', 48 | "Mousetrap": '../vendors/mousetrap/mousetrap', 49 | "lunr": '../vendors/lunr.js/lunr', 50 | "URIjs": '../vendors/URIjs/src/', 51 | "ace": '../vendors/ace-builds/src-noconflict/' 52 | }, 53 | shim: { 54 | 'jQuery': { 55 | exports: '$' 56 | }, 57 | 'lodash': { 58 | exports: '_' 59 | }, 60 | 'Mousetrap': { 61 | exports: 'Mousetrap' 62 | }, 63 | 'lunr': { 64 | exports: 'lunr' 65 | } 66 | } 67 | } 68 | } 69 | }, 70 | copy: { 71 | vendors: { 72 | files: [ 73 | { 74 | expand: true, 75 | cwd: 'theme/vendors/fontawesome/fonts/', 76 | src: ['**'], 77 | dest: 'theme/assets/fonts/fontawesome/', 78 | filter: 'isFile' 79 | } 80 | ] 81 | } 82 | }, 83 | browserify: { 84 | dist: { 85 | files: { 86 | 'gitbook.js': [ 87 | './lib/parse/index.js' 88 | ], 89 | }, 90 | options: { 91 | postBundleCB: function (err, src, next) { 92 | return next(null, '(function () { var define = undefined; '+src+'; })();') 93 | }, 94 | browserifyOptions: { 95 | 'standalone': "gitbook" 96 | } 97 | } 98 | } 99 | }, 100 | uglify: { 101 | dist: { 102 | files: { 103 | 'gitbook.min.js': ['gitbook.js'] 104 | } 105 | } 106 | } 107 | }); 108 | 109 | grunt.registerTask("bower-install", [ "bower-install-simple" ]); 110 | 111 | // Build 112 | grunt.registerTask('build', [ 113 | 'bower-install', 114 | 'less', 115 | 'requirejs', 116 | 'copy:vendors' 117 | ]); 118 | 119 | // Bundle the library 120 | grunt.registerTask('bundle', [ 121 | 'browserify', 122 | 'uglify' 123 | ]); 124 | 125 | grunt.registerTask('default', [ 126 | 'build', 127 | 'bundle' 128 | ]); 129 | }; -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Q = require('q'); 3 | var _ = require('lodash'); 4 | var fs = require('fs'); 5 | 6 | var utils = require('./utils'); 7 | var generate = require("../lib/generate"); 8 | var parse = require("../lib/parse"); 9 | var generators = require("../lib/generate").generators; 10 | 11 | var buildCommand = function(command) { 12 | return command 13 | .option('-v, --verbose', 'Activate verbose mode, useful for debugging errors') 14 | .option('-o, --output ', 'Path to output directory, defaults to ./_book') 15 | .option('-f, --format ', 'Change generation format, defaults to site, availables are: '+_.keys(generators).join(", ")) 16 | .option('--config ', 'Configuration file to use, defaults to book.js or book.json'); 17 | }; 18 | 19 | 20 | var buildEbookCommand = function(command) { 21 | return buildCommand(command) 22 | .option('-c, --cover ', 'Cover image, default is cover.jpg if exists'); 23 | }; 24 | 25 | var makeBuildFunc = function(converter) { 26 | return function(dir, options) { 27 | dir = dir || process.cwd(); 28 | outputDir = options.output; 29 | 30 | // Set debugging 31 | if(options.verbose) { 32 | process.env.DEBUG = "true"; 33 | } 34 | 35 | console.log('Starting build ...'); 36 | return converter( 37 | _.extend({}, options || {}, { 38 | input: dir, 39 | output: outputDir, 40 | generator: options.format, 41 | configFile: options.config 42 | }) 43 | ) 44 | .then(function(output) { 45 | console.log("Successfully built!"); 46 | return output; 47 | }); 48 | }; 49 | }; 50 | 51 | module.exports = { 52 | folder: makeBuildFunc(generate.folder), 53 | file: makeBuildFunc(generate.file), 54 | command: buildCommand, 55 | commandEbook: buildEbookCommand 56 | }; 57 | -------------------------------------------------------------------------------- /bin/gitbook.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var Q = require('q'); 4 | var _ = require('lodash'); 5 | var path = require('path'); 6 | var prog = require('commander'); 7 | var tinylr = require('tiny-lr-fork'); 8 | 9 | var pkg = require('../package.json'); 10 | var genbook = require("../lib/generate"); 11 | var initDir = require("../lib/generate/init"); 12 | var fs = require('../lib/generate/fs'); 13 | 14 | var utils = require('./utils'); 15 | var action = utils.action; 16 | var build = require('./build'); 17 | var Server = require('./server'); 18 | var platform = require("./platform"); 19 | 20 | // General options 21 | prog 22 | .version(pkg.version); 23 | 24 | build.command(prog.command('build [source_dir]')) 25 | .description('Build a gitbook from a directory') 26 | .action(action(build.folder)); 27 | 28 | build.command(prog.command('serve [source_dir]')) 29 | .description('Build then serve a gitbook from a directory') 30 | .option('-p, --port ', 'Port for server to listen on', 4000) 31 | .option('--no-watch', 'Disable restart with file watching') 32 | .action(action(function(dir, options) { 33 | var server = new Server(); 34 | 35 | // init livereload server 36 | var lrOptions = {port: 35729}; 37 | var lrServer = tinylr(lrOptions); 38 | var lrPath = undefined; 39 | lrServer.listen(lrOptions.port, function(err) { 40 | if (err) { return console.log(err); } 41 | console.log('Live reload server started on port: ' + lrOptions.port); 42 | }); 43 | 44 | var generate = function() { 45 | if (server.isRunning()) { 46 | console.log("Stopping server"); 47 | } 48 | 49 | return server.stop() 50 | .then(function() { 51 | return build.folder(dir, _.extend(options || {}, { 52 | defaultsPlugins: ["livereload"] 53 | })); 54 | }) 55 | .then(function(_options) { 56 | console.log(); 57 | console.log('Starting server ...'); 58 | return server.start(_options.output, options.port) 59 | .then(function() { 60 | console.log('Serving book on http://localhost:'+options.port); 61 | 62 | if (lrPath) { 63 | // trigger livereload 64 | lrServer.changed({body:{files:[lrPath]}}) 65 | } 66 | 67 | if (!options.watch) return; 68 | return utils.watch(_options.input) 69 | .then(function(filepath) { 70 | // set livereload path 71 | lrPath = filepath; 72 | console.log("Restart after change in files"); 73 | console.log(''); 74 | return generate(); 75 | }) 76 | }) 77 | }) 78 | .fail(utils.logError); 79 | }; 80 | 81 | console.log('Press CTRL+C to quit ...'); 82 | console.log('') 83 | 84 | return generate(); 85 | })); 86 | 87 | build.commandEbook(prog.command('install [source_dir]')) 88 | .description('Install plugins for a book') 89 | .action(action(function(dir, options) { 90 | dir = dir || process.cwd(); 91 | 92 | console.log("Install plugins in", dir); 93 | 94 | return genbook.config.read({ 95 | input: dir 96 | }) 97 | .then(function(options) { 98 | return genbook.Plugin.install(options); 99 | }) 100 | .then(function() { 101 | console.log("Successfully installed plugins!"); 102 | }); 103 | })); 104 | 105 | build.commandEbook(prog.command('pdf [source_dir]')) 106 | .description('Build a gitbook as a PDF') 107 | .action(action(function(dir, options) { 108 | return build.file(dir, _.extend(options, { 109 | extension: "pdf", 110 | format: "ebook" 111 | })); 112 | })); 113 | 114 | build.commandEbook(prog.command('epub [source_dir]')) 115 | .description('Build a gitbook as a ePub book') 116 | .action(action(function(dir, options) { 117 | return build.file(dir, _.extend(options, { 118 | extension: "epub", 119 | format: "ebook" 120 | })); 121 | })); 122 | 123 | build.commandEbook(prog.command('mobi [source_dir]')) 124 | .description('Build a gitbook as a Mobi book') 125 | .action(action(function(dir, options) { 126 | return build.file(dir, _.extend(options, { 127 | extension: "mobi", 128 | format: "ebook" 129 | })); 130 | })); 131 | 132 | prog 133 | .command('init [source_dir]') 134 | .description('Create files and folders based on contents of SUMMARY.md') 135 | .action(action(function(dir) { 136 | dir = dir || process.cwd(); 137 | return initDir(dir); 138 | })); 139 | 140 | prog 141 | .command('publish [source_dir]') 142 | .description('Publish content to the associated gitbook.com book') 143 | .action(action(function(dir) { 144 | dir = dir || process.cwd(); 145 | return platform.publish(dir); 146 | })); 147 | 148 | prog 149 | .command('git:remote [source_dir] [book_id]') 150 | .description('Adds a git remote to a book repository') 151 | .action(action(function(dir, bookId) { 152 | dir = dir || process.cwd(); 153 | return platform.remote(dir, bookId); 154 | })); 155 | 156 | // Parse and fallback to help if no args 157 | if(_.isEmpty(prog.parse(process.argv).args) && process.argv.length === 2) { 158 | prog.help(); 159 | } 160 | -------------------------------------------------------------------------------- /bin/platform.js: -------------------------------------------------------------------------------- 1 | var Q = require("q"); 2 | var utils = require("./utils"); 3 | 4 | var publish = function(folder) { 5 | if (!folder) { 6 | console.log("Need a repository folder"); 7 | return process.exit(-1); 8 | } 9 | 10 | utils.gitCmd("push", ["gitbook", "master"]) 11 | .then(function(out) { 12 | console.log(out.stdout); 13 | }, function(err) { 14 | if (err.code == 128) { 15 | console.log("No book on gitbook.com is configured with this git repository."); 16 | console.log("Run 'gitbook git:remote username/book' to intialize this repository."); 17 | } else { 18 | console.log(err.message); 19 | } 20 | process.exit(-1); 21 | }); 22 | }; 23 | 24 | var remote = function(folder, bookId) { 25 | if (!folder || !bookId) { 26 | console.log("Need a repository folder and a book id"); 27 | return process.exit(-1); 28 | } 29 | 30 | var url = "https://git.gitbook.com/"+bookId+".git"; 31 | var addRemote = function() { 32 | return utils.gitCmd("remote", ["add", "gitbook", url]); 33 | } 34 | 35 | addRemote() 36 | .fail(function(err) { 37 | if (err.code == 128) { 38 | return utils.gitCmd("remote", ["rm", "gitbook"]).then(addRemote); 39 | } 40 | return Q.reject(err); 41 | }) 42 | .then(function(out) { 43 | console.log("Book remote '"+url+"' added to the folder"); 44 | }, function(err) { 45 | console.log(err.message); 46 | process.exit(-1); 47 | }); 48 | }; 49 | 50 | module.exports = { 51 | publish: publish, 52 | remote: remote 53 | }; 54 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var _ = require('lodash'); 3 | 4 | var events = require('events'); 5 | var http = require('http'); 6 | var send = require('send'); 7 | var util = require('util'); 8 | var url = require('url'); 9 | 10 | var Server = function() { 11 | this.running = null; 12 | this.dir = null; 13 | this.port = 0; 14 | this.sockets = []; 15 | }; 16 | util.inherits(Server, events.EventEmitter); 17 | 18 | // Return true if the server is running 19 | Server.prototype.isRunning = function() { 20 | return this.running != null; 21 | }; 22 | 23 | // Stop the server 24 | Server.prototype.stop = function() { 25 | var that = this; 26 | if (!this.isRunning()) return Q(); 27 | 28 | var d = Q.defer(); 29 | this.running.close(function(err) { 30 | that.running = null; 31 | that.emit("state", false); 32 | 33 | if (err) d.reject(err); 34 | else d.resolve(); 35 | }); 36 | 37 | for (var i = 0; i < this.sockets.length; i++) { 38 | this.sockets[i].destroy(); 39 | } 40 | 41 | return d.promise; 42 | }; 43 | 44 | Server.prototype.start = function(dir, port) { 45 | var that = this, pre = Q(); 46 | port = port || 8004; 47 | 48 | if (that.isRunning()) pre = this.stop(); 49 | return pre 50 | .then(function() { 51 | var d = Q.defer(); 52 | 53 | that.running = http.createServer(function(req, res){ 54 | // Render error 55 | function error(err) { 56 | res.statusCode = err.status || 500; 57 | res.end(err.message); 58 | } 59 | 60 | // Redirect to directory's index.html 61 | function redirect() { 62 | res.statusCode = 301; 63 | res.setHeader('Location', req.url + '/'); 64 | res.end('Redirecting to ' + req.url + '/'); 65 | } 66 | 67 | // Send file 68 | send(req, url.parse(req.url).pathname) 69 | .root(dir) 70 | .on('error', error) 71 | .on('directory', redirect) 72 | .pipe(res); 73 | }); 74 | 75 | that.running.on('connection', function (socket) { 76 | that.sockets.push(socket); 77 | socket.setTimeout(4000); 78 | socket.on('close', function () { 79 | that.sockets.splice(that.sockets.indexOf(socket), 1); 80 | }); 81 | }); 82 | 83 | that.running.listen(port, function(err) { 84 | if (err) return d.reject(err); 85 | 86 | that.port = port; 87 | that.dir = dir; 88 | that.emit("state", true); 89 | d.resolve(); 90 | }); 91 | 92 | return d.promise; 93 | }); 94 | } 95 | 96 | module.exports = Server; 97 | -------------------------------------------------------------------------------- /bin/utils.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var _ = require('lodash'); 3 | 4 | var exec = require('child_process').exec; 5 | var http = require('http'); 6 | var send = require('send'); 7 | 8 | var path = require('path'); 9 | var Gaze = require('gaze').Gaze; 10 | 11 | function watch(dir) { 12 | var d = Q.defer(); 13 | dir = path.resolve(dir); 14 | 15 | var gaze = new Gaze("**/*.md", { 16 | cwd: dir 17 | }); 18 | 19 | gaze.once("all", function(e, filepath) { 20 | gaze.close(); 21 | 22 | d.resolve(filepath); 23 | }); 24 | gaze.once("error", function(err) { 25 | gaze.close(); 26 | 27 | d.reject(err); 28 | }); 29 | 30 | return d.promise; 31 | } 32 | 33 | // exit wraps a promise 34 | // and forcefully quits the program when the promise is resolved 35 | function exit(promise) { 36 | promise 37 | .then(function() { 38 | // Prevent badly behaving plugins 39 | // from making the process hang 40 | process.exit(0); 41 | }, function(err) { 42 | // Log error 43 | logError(err); 44 | 45 | // Exit process with failure code 46 | process.exit(-1); 47 | }); 48 | } 49 | 50 | // CLI action wrapper, calling exit when finished 51 | function action(f) { 52 | return function() { 53 | // Call func and get optional promise 54 | var p = f.apply(null, arguments); 55 | 56 | // Exit process 57 | return exit(Q(p)); 58 | } 59 | } 60 | 61 | function logError(err) { 62 | var message = err.message || err; 63 | if (process.env.DEBUG != null) message = err.stack || message; 64 | console.log(message); 65 | return Q.reject(err); 66 | }; 67 | 68 | function runGitCommand(command, args) { 69 | var d = Q.defer(), child; 70 | args = ["git", command].concat(args).join(" "); 71 | 72 | child = exec(args, function (error, stdout, stderr) { 73 | if (error !== null) { 74 | error.stdout = stdout; 75 | error.stderr = stderr; 76 | d.reject(error); 77 | } else { 78 | d.resolve({ 79 | stdout: stdout, 80 | stderr: stderr 81 | }) 82 | } 83 | }); 84 | 85 | return d.promise; 86 | }; 87 | 88 | 89 | // Exports 90 | module.exports = { 91 | exit: exit, 92 | action: action, 93 | watch: watch, 94 | logError: logError, 95 | gitCmd: runGitCommand 96 | }; 97 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitBook", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "jquery": "2.1.1", 6 | "lodash": "2.4.1", 7 | "requirejs": "2.1.11", 8 | "URIjs": "1.13.1", 9 | "mousetrap": "1.4.6", 10 | "lunr.js": "codepiano/lunr.js", 11 | "fontawesome": "4.1.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/generate/config.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var _ = require('lodash'); 3 | var path = require('path'); 4 | 5 | // Default configuration for gitbook 6 | var CONFIG = { 7 | // Folders to use for output 8 | // Caution: it overrides the value from the command line 9 | // It's not advised this option in the book.json 10 | "output": null, 11 | 12 | // Generator to use for building 13 | // Caution: it overrides the value from the command line 14 | // It's not advised this option in the book.json 15 | "generator": "site", 16 | 17 | // Configuration file to use 18 | "configFile": "book", 19 | 20 | // Book metadats (somes are extracted from the README by default) 21 | "title": null, 22 | "description": null, 23 | "keywords": "", 24 | "isbn": null, 25 | 26 | // For ebook format, the extension to use for generation (default is detected from output extension) 27 | // "epub", "pdf", "mobi" 28 | // Caution: it overrides the value from the command line 29 | // It's not advised this option in the book.json 30 | "extension": null, 31 | 32 | // Plugins list, can contain "-name" for removing default plugins 33 | "plugins": [], 34 | 35 | // Global configuration for plugins 36 | "pluginsConfig": { 37 | "fontSettings": { 38 | "theme": null, //"sepia", "night" or "white", 39 | "family": "sans",// "serif" or "sans", 40 | "size": 2 // 1 - 4 41 | } 42 | }, 43 | 44 | // Variables for templating 45 | "variables": {}, 46 | 47 | // Set another theme with your own layout 48 | // It's recommended to use plugins or add more options for default theme, though 49 | // See https://github.com/GitbookIO/gitbook/issues/209 50 | "theme": path.resolve(__dirname, '../../theme'), 51 | 52 | // Links in template (null: default, false: remove, string: new value) 53 | "links": { 54 | // Custom links at top of sidebar 55 | "sidebar": { 56 | //"Custom link name": "https://customlink.com" 57 | }, 58 | 59 | // Sharing links 60 | "sharing": { 61 | "google": null, 62 | "facebook": null, 63 | "twitter": null, 64 | "weibo": null, 65 | "all": null 66 | } 67 | }, 68 | 69 | // CSS Styles 70 | "styles": { 71 | "website": "styles/website.css", 72 | "ebook": "styles/ebook.css", 73 | "pdf": "styles/pdf.css", 74 | "mobi": "styles/mobi.css", 75 | "epub": "styles/epub.css" 76 | }, 77 | 78 | // Options for PDF generation 79 | "pdf": { 80 | // Add toc at the end of the file 81 | "toc": true, 82 | 83 | // Add page numbers to the bottom of every page 84 | "pageNumbers": false, 85 | 86 | // Font size for the file content 87 | "fontSize": 12, 88 | 89 | // Paper size for the pdf 90 | // Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’] 91 | "paperSize": "a4", 92 | 93 | // Margin (in pts) 94 | // Note: 72 pts equals 1 inch 95 | "margin": { 96 | "right": 62, 97 | "left": 62, 98 | "top": 36, 99 | "bottom": 36 100 | }, 101 | 102 | //Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_. 103 | "headerTemplate": "", 104 | 105 | //Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_. 106 | "footerTemplate": "" 107 | } 108 | }; 109 | 110 | // Return complete configuration 111 | var defaultsConfig = function(options) { 112 | return _.merge(options || {}, CONFIG, _.defaults); 113 | }; 114 | 115 | // Read configuration from book.json 116 | var readConfig = function(options) { 117 | options = defaultsConfig(options); 118 | 119 | return Q() 120 | .then(function() { 121 | try { 122 | var _config = require(path.resolve(options.input, options.configFile)); 123 | options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins', 'generator')); 124 | } 125 | catch(err) { 126 | // No config file: not a big deal 127 | return Q(); 128 | } 129 | }) 130 | .thenResolve(options); 131 | }; 132 | 133 | module.exports = { 134 | CONFIG: CONFIG, 135 | defaults: defaultsConfig, 136 | read: readConfig 137 | } 138 | 139 | -------------------------------------------------------------------------------- /lib/generate/ebook/index.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | var path = require("path"); 3 | var Q = require("q"); 4 | var _ = require("lodash"); 5 | var exec = require('child_process').exec; 6 | 7 | var fs = require('graceful-fs'); 8 | var parse = require("../../parse"); 9 | var BaseGenerator = require("../page"); 10 | var stringUtils = require("../../utils/string"); 11 | 12 | var Generator = function() { 13 | BaseGenerator.apply(this, arguments); 14 | 15 | // eBook format 16 | this.ebookFormat = this.options.extension || path.extname(this.options.output).replace("\.", "") || "pdf"; 17 | 18 | // Styles to use 19 | this.styles = ["ebook", this.ebookFormat]; 20 | }; 21 | util.inherits(Generator, BaseGenerator); 22 | 23 | Generator.prototype.finish = function() { 24 | var that = this; 25 | 26 | return BaseGenerator.prototype.finish.apply(this) 27 | .then(function() { 28 | var d = Q.defer(); 29 | 30 | if (!that.options.cover && fs.existsSync(path.join(that.options.output, "cover.jpg"))) { 31 | that.options.cover = path.join(that.options.output, "cover.jpg"); 32 | } 33 | 34 | var _options = { 35 | "--cover": that.options.cover, 36 | "--title": that.options.title, 37 | "--comments": that.options.description, 38 | "--isbn": that.options.isbn, 39 | "--authors": that.options.author, 40 | "--publisher": "GitBook", 41 | "--chapter": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter ')]", 42 | "--chapter-mark": "pagebreak", 43 | "--page-breaks-before": "/", 44 | "--level1-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-1 ')]", 45 | "--level2-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-2 ')]", 46 | "--level3-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-3 ')]", 47 | "--no-chapters-in-toc": true, 48 | "--max-levels": "1", 49 | "--breadth-first": true 50 | }; 51 | 52 | if (that.ebookFormat == "pdf") { 53 | var pdfOptions = that.options.pdf; 54 | 55 | _.extend(_options, { 56 | "--margin-left": String(pdfOptions.margin.left), 57 | "--margin-right": String(pdfOptions.margin.right), 58 | "--margin-top": String(pdfOptions.margin.top), 59 | "--margin-bottom": String(pdfOptions.margin.bottom), 60 | "--pdf-default-font-size": String(pdfOptions.fontSize), 61 | "--pdf-mono-font-size": String(pdfOptions.fontSize), 62 | "--paper-size": String(pdfOptions.paperSize), 63 | "--pdf-page-numbers": Boolean(pdfOptions.pageNumbers), 64 | "--pdf-header-template": String(pdfOptions.headerTemplate), 65 | "--pdf-footer-template": String(pdfOptions.footerTemplate) 66 | }); 67 | } 68 | 69 | var command = [ 70 | "ebook-convert", 71 | path.join(that.options.output, "SUMMARY.html"), 72 | path.join(that.options.output, "index."+that.ebookFormat), 73 | stringUtils.optionsToShellArgs(_options) 74 | ].join(" "); 75 | 76 | exec(command, function (error, stdout, stderr) { 77 | if (error) { 78 | if (error.code == 127) { 79 | error.message = "Need to install ebook-convert from Calibre"; 80 | } else { 81 | error.message = error.message + " "+stdout; 82 | } 83 | return d.reject(error); 84 | } 85 | d.resolve(); 86 | }); 87 | 88 | return d.promise; 89 | }); 90 | }; 91 | 92 | module.exports = Generator; 93 | -------------------------------------------------------------------------------- /lib/generate/fs.js: -------------------------------------------------------------------------------- 1 | var Q = require("q"); 2 | var fs = require('graceful-fs'); 3 | var fsExtra = require("fs-extra"); 4 | var Ignore = require("fstream-ignore"); 5 | 6 | var getFiles = function(path) { 7 | var d = Q.defer(); 8 | 9 | // Our list of files 10 | var files = []; 11 | 12 | var ig = Ignore({ 13 | path: path, 14 | ignoreFiles: ['.ignore', '.gitignore', '.bookignore'] 15 | }); 16 | 17 | // Add extra rules to ignore common folders 18 | ig.addIgnoreRules([ 19 | // Skip Git stuff 20 | '.git/', 21 | '.gitignore', 22 | 23 | // Skip OS X meta data 24 | '.DS_Store', 25 | 26 | // Skip stuff installed by plugins 27 | 'node_modules', 28 | 29 | // Skip book outputs 30 | '*.pdf', 31 | '*.epub', 32 | '*.mobi', 33 | 34 | // Skip config files 35 | '.ignore', 36 | '.bookignore', 37 | 'book.json', 38 | ], '__custom_stuff'); 39 | 40 | // Push each file to our list 41 | ig.on('child', function (c) { 42 | files.push( 43 | c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '') 44 | ); 45 | }); 46 | 47 | ig.on('end', function() { 48 | // Normalize paths on Windows 49 | if(process.platform === 'win32') { 50 | return d.resolve(files.map(function(file) { 51 | return file.replace(/\\/g, '/'); 52 | })); 53 | } 54 | 55 | // Simply return paths otherwise 56 | return d.resolve(files); 57 | }); 58 | 59 | ig.on('error', d.reject); 60 | 61 | return d.promise; 62 | }; 63 | 64 | module.exports = { 65 | list: getFiles, 66 | readFile: Q.denodeify(fs.readFile), 67 | //writeFile: Q.denodeify(fs.writeFile), 68 | writeFile: function(filename, data, options) { 69 | var d = Q.defer(); 70 | 71 | try { 72 | fs.writeFileSync(filename, data, options) 73 | } catch(err) { 74 | d.reject(err); 75 | } 76 | d.resolve(); 77 | 78 | 79 | return d.promise; 80 | }, 81 | mkdirp: Q.denodeify(fsExtra.mkdirp), 82 | copy: Q.denodeify(fsExtra.copy), 83 | remove: Q.denodeify(fsExtra.remove), 84 | symlink: Q.denodeify(fsExtra.symlink), 85 | exists: function(path) { 86 | var d = Q.defer(); 87 | fs.exists(path, d.resolve); 88 | return d.promise; 89 | }, 90 | existsSync: fs.existsSync, 91 | readFileSync: fs.readFileSync.bind(fs) 92 | }; 93 | -------------------------------------------------------------------------------- /lib/generate/generator.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var path = require("path"); 3 | var Q = require("q"); 4 | var fs = require("./fs"); 5 | 6 | var Plugin = require("./plugin"); 7 | 8 | var BaseGenerator = function(options) { 9 | this.options = options; 10 | 11 | // Base for assets in plugins 12 | this.pluginAssetsBase = "book"; 13 | 14 | this.options.plugins = Plugin.normalizeNames(this.options.plugins); 15 | this.options.plugins = _.union(this.options.plugins, this.options.defaultsPlugins); 16 | this.plugins = []; 17 | }; 18 | 19 | BaseGenerator.prototype.callHook = function(name, data) { 20 | return this.plugins.hook(name, data); 21 | }; 22 | 23 | // Sets up generator 24 | BaseGenerator.prototype.load = function() { 25 | return this.loadPlugins(); 26 | }; 27 | 28 | BaseGenerator.prototype.loadPlugins = function() { 29 | var that = this; 30 | 31 | return Plugin.fromList(this.options.plugins, this.options.input, this, { 32 | assetsBase: this.pluginAssetsBase 33 | }) 34 | .then(function(_plugins) { 35 | that.plugins = _plugins; 36 | 37 | return that.callHook("init"); 38 | }); 39 | }; 40 | 41 | BaseGenerator.prototype.convertFile = function(content, input) { 42 | return Q.reject(new Error("Could not convert "+input)); 43 | }; 44 | 45 | BaseGenerator.prototype.transferFile = function(input) { 46 | return fs.copy( 47 | path.join(this.options.input, input), 48 | path.join(this.options.output, input) 49 | ); 50 | }; 51 | 52 | BaseGenerator.prototype.transferFolder = function(input) { 53 | return fs.mkdirp( 54 | path.join(this.options.output, input) 55 | ); 56 | }; 57 | 58 | BaseGenerator.prototype.copyCover = function() { 59 | var that = this; 60 | 61 | return Q.all([ 62 | fs.copy(path.join(this.options.input, "cover.jpg"), path.join(this.options.output, "cover.jpg")), 63 | fs.copy(path.join(this.options.input, "cover_small.jpg"), path.join(this.options.output, "cover_small.jpg")) 64 | ]) 65 | .fail(function() { 66 | // If orignally from multi-lang, try copy from originalInput 67 | if (!that.options.originalInput) return; 68 | 69 | return Q.all([ 70 | fs.copy(path.join(that.options.originalInput, "cover.jpg"), path.join(that.options.output, "cover.jpg")), 71 | fs.copy(path.join(that.options.originalInput, "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg")) 72 | ]); 73 | }) 74 | .fail(function(err) { 75 | return Q(); 76 | }); 77 | }; 78 | 79 | BaseGenerator.prototype.langsIndex = function(langs) { 80 | return Q.reject(new Error("Langs index is not supported in this generator")); 81 | }; 82 | 83 | BaseGenerator.prototype.finish = function() { 84 | return Q.reject(new Error("Could not finish generation")); 85 | }; 86 | 87 | module.exports = BaseGenerator; 88 | -------------------------------------------------------------------------------- /lib/generate/init.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var _ = require('lodash'); 3 | 4 | var path = require('path'); 5 | 6 | var fs = require('./fs'); 7 | var parse = require('../parse'); 8 | 9 | 10 | // Extract paths out of a summary 11 | function paths(summary) { 12 | return _.reduce(summary.chapters, function(accu, chapter) { 13 | return accu.concat( 14 | _.filter([chapter.path].concat(_.pluck(chapter.articles, 'path'))) 15 | ); 16 | }, []); 17 | } 18 | 19 | // Get the parent folders out of a group of files 20 | function folders(files) { 21 | return _.chain(files) 22 | .map(function(file) { 23 | return path.dirname(file); 24 | }) 25 | .uniq() 26 | .value(); 27 | } 28 | 29 | function initDir(dir) { 30 | return fs.readFile(path.join(dir, 'SUMMARY.md'), 'utf8') 31 | .then(function(src) { 32 | // Parse summary 33 | return parse.summary(src); 34 | }) 35 | .then(function(summary) { 36 | // Extract paths from summary 37 | return paths(summary); 38 | }) 39 | .then(function(paths) { 40 | // Convert to absolute paths 41 | return _.map(paths, function(file) { 42 | return path.resolve(file); 43 | }); 44 | }) 45 | .then(function(files) { 46 | // Create folders 47 | return Q.all(_.map(folders(files), function(folder) { 48 | return fs.mkdirp(folder); 49 | })) 50 | .then(_.constant(files)); 51 | }) 52 | .then(function(files) { 53 | // Create files that don't exist 54 | return Q.all(_.map(files, function(file) { 55 | return fs.exists(file) 56 | .then(function(exists) { 57 | if(exists) return; 58 | return fs.writeFile(file, ''); 59 | }); 60 | })); 61 | }) 62 | .fail(function(err) { 63 | console.error(err.stack); 64 | }); 65 | } 66 | 67 | 68 | // Exports 69 | module.exports = initDir; 70 | -------------------------------------------------------------------------------- /lib/generate/json/index.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | var path = require("path"); 3 | var Q = require("q"); 4 | var _ = require("lodash"); 5 | 6 | var fs = require("../fs"); 7 | var parse = require("../../parse"); 8 | var BaseGenerator = require("../generator"); 9 | 10 | 11 | var Generator = function() { 12 | BaseGenerator.apply(this, arguments); 13 | }; 14 | util.inherits(Generator, BaseGenerator); 15 | 16 | Generator.prototype.transferFile = function(input) { 17 | // ignore 18 | }; 19 | 20 | Generator.prototype.convertFile = function(content, input) { 21 | var that = this; 22 | var json = { 23 | progress: parse.progress(this.options.navigation, input) 24 | }; 25 | 26 | return Q() 27 | .then(function() { 28 | return parse.page(content, { 29 | dir: path.dirname(input) || '/' 30 | }); 31 | }) 32 | .then(function(parsed) { 33 | json.lexed = parsed.lexed; 34 | json.sections = parsed.sections; 35 | }) 36 | .then(function() { 37 | return fs.writeFile( 38 | path.join(that.options.output, input.replace(".md", ".json")), 39 | JSON.stringify(json, null, 4) 40 | ); 41 | }); 42 | }; 43 | 44 | // Generate languages index 45 | // Contains the first languages readme and langs infos 46 | Generator.prototype.langsIndex = function(langs) { 47 | var that = this; 48 | 49 | if (langs.list.length == 0) return Q.reject("Need at least one language"); 50 | 51 | var mainLang = _.first(langs.list).lang; 52 | console.log("Main language is", mainLang); 53 | 54 | return Q() 55 | .then(function() { 56 | return fs.readFile( 57 | path.join(that.options.output, mainLang, "README.json") 58 | ); 59 | }) 60 | .then(function(content) { 61 | var json = JSON.parse(content); 62 | _.extend(json, { 63 | langs: langs.list 64 | }); 65 | 66 | return fs.writeFile( 67 | path.join(that.options.output, "README.json"), 68 | JSON.stringify(json, null, 4) 69 | ); 70 | }); 71 | }; 72 | 73 | Generator.prototype.finish = function() { 74 | // ignore 75 | }; 76 | 77 | module.exports = Generator; 78 | -------------------------------------------------------------------------------- /lib/generate/page/index.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var util = require("util"); 3 | var path = require("path"); 4 | var Q = require("q"); 5 | var swig = require("../template"); 6 | 7 | var fs = require("../fs"); 8 | var parse = require("../../parse"); 9 | var BaseGenerator = require("../site"); 10 | 11 | var Generator = function() { 12 | BaseGenerator.apply(this, arguments); 13 | 14 | // Styles to use 15 | this.styles = ["ebook"]; 16 | 17 | // Base for assets in plugins 18 | this.pluginAssetsBase = "ebook"; 19 | 20 | // List of pages content 21 | this.pages = {}; 22 | }; 23 | util.inherits(Generator, BaseGenerator); 24 | 25 | Generator.prototype.loadTemplates = function() { 26 | this.template = swig.compileFile( 27 | this.plugins.template("ebook:page") || path.resolve(this.options.theme, 'templates/ebook/page.html') 28 | ); 29 | this.summaryTemplate = swig.compileFile( 30 | this.plugins.template("ebook:sumary") || path.resolve(this.options.theme, 'templates/ebook/summary.html') 31 | ); 32 | this.glossaryTemplate = swig.compileFile( 33 | this.plugins.template("ebook:glossary") || path.resolve(this.options.theme, 'templates/ebook/glossary.html') 34 | ); 35 | }; 36 | 37 | // Generate table of contents 38 | Generator.prototype.writeToc = function() { 39 | var that = this; 40 | var basePath = "."; 41 | 42 | return this._writeTemplate(this.summaryTemplate, { 43 | toc: parse.progress(this.options.navigation, "README.md").chapters, 44 | basePath: basePath, 45 | staticBase: path.join(basePath, "gitbook"), 46 | }, path.join(this.options.output, "SUMMARY.html")); 47 | }; 48 | 49 | Generator.prototype.finish = function() { 50 | var that = this; 51 | var basePath = "."; 52 | var output = path.join(this.options.output, "index.html"); 53 | 54 | var progress = parse.progress(this.options.navigation, "README.md"); 55 | 56 | return Q() 57 | 58 | // Write table of contents 59 | .then(function() { 60 | return that.writeToc(); 61 | }) 62 | 63 | // Write glossary 64 | .then(function() { 65 | return that.writeGlossary(); 66 | }) 67 | 68 | // Copy cover 69 | .then(function() { 70 | return that.copyCover(); 71 | }) 72 | 73 | // Copy assets 74 | .then(function() { 75 | return that.copyAssets(); 76 | }); 77 | }; 78 | 79 | // Generate languages index 80 | Generator.prototype.langsIndex = function(langs) { 81 | return Q(); 82 | }; 83 | 84 | module.exports = Generator; 85 | -------------------------------------------------------------------------------- /lib/generate/site/glossary_indexer.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var kramed = require('kramed'); 3 | var textRenderer = require('kramed-text-renderer'); 4 | 5 | var entryId = require('../../parse/glossary').entryId; 6 | 7 | 8 | function Indexer(glossary) { 9 | if(!(this instanceof Indexer)) { 10 | return new Indexer(glossary); 11 | } 12 | 13 | _.bindAll(this); 14 | 15 | this.glossary = glossary || []; 16 | 17 | this.glossaryTerms = _.pluck(this.glossary, "id"); 18 | 19 | // Regex for searching for terms through body 20 | this.termsRegex = new RegExp( 21 | // Match any of the terms 22 | "("+ 23 | this.glossaryTerms.map(regexEscape).join('|') + 24 | ")", 25 | 26 | // Flags 27 | "gi" 28 | ); 29 | 30 | // page url => terms 31 | this.idx = { 32 | /* 33 | "a/b.html": ["one word", "second word"] 34 | */ 35 | }; 36 | 37 | // term => page urls 38 | this.invertedIdx = { 39 | /* 40 | "word1": ["page1.html", "page2.html"] 41 | */ 42 | }; 43 | 44 | // Use text renderer 45 | this.renderer = textRenderer(); 46 | } 47 | 48 | Indexer.prototype.text = function(nodes) { 49 | // Copy section 50 | var section = _.toArray(nodes); 51 | 52 | // kramed's Render expects this, we don't use it yet 53 | section.links = {}; 54 | 55 | var options = _.extend({}, kramed.defaults, { 56 | renderer: this.renderer 57 | }); 58 | 59 | return kramed.parser(section, options); 60 | }; 61 | 62 | // Add page to glossary index 63 | Indexer.prototype.add = function(sections, url) { 64 | if(!(this.glossary && this.glossary.length > 0)) { 65 | return; 66 | } 67 | 68 | var textblob = 69 | _.where(sections, { type: 'normal' }) 70 | .map(this.text) 71 | .join('\n'); 72 | 73 | var matches = _(textblob.match(this.termsRegex) || []) 74 | .map(entryId) 75 | .uniq() 76 | .value(); 77 | 78 | // Add idx for book 79 | this.idx[url] = matches; 80 | 81 | // Add to inverted idx 82 | matches.forEach(function(match) { 83 | if(!this.invertedIdx[match]) { 84 | this.invertedIdx[match] = []; 85 | } 86 | this.invertedIdx[match].push(url); 87 | }.bind(this)); 88 | }; 89 | 90 | // Dump index as a string 91 | Indexer.prototype.dump = function() { 92 | return JSON.stringify(this.idx); 93 | }; 94 | 95 | 96 | function regexEscape(s) { 97 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 98 | } 99 | 100 | // Exports 101 | module.exports = Indexer; 102 | -------------------------------------------------------------------------------- /lib/generate/site/search_indexer.js: -------------------------------------------------------------------------------- 1 | var Q = require("q"); 2 | var _ = require("lodash"); 3 | 4 | var lunr = require('lunr'); 5 | var kramed = require('kramed'); 6 | var textRenderer = require('kramed-text-renderer'); 7 | 8 | 9 | function Indexer() { 10 | if(!(this instanceof Indexer)) { 11 | return new Indexer(); 12 | } 13 | 14 | _.bindAll(this); 15 | 16 | // Setup lunr index 17 | this.idx = lunr(function () { 18 | this.ref('url'); 19 | 20 | this.field('title', { boost: 10 }); 21 | this.field('body'); 22 | }); 23 | 24 | this.renderer = textRenderer(); 25 | } 26 | 27 | Indexer.prototype.text = function(nodes) { 28 | // Copy section 29 | var section = _.toArray(nodes); 30 | 31 | // kramed's Render expects this, we don't use it yet 32 | section.links = {}; 33 | 34 | var options = _.extend({}, kramed.defaults, { 35 | renderer: this.renderer 36 | }); 37 | 38 | return kramed.parser(section, options); 39 | }; 40 | 41 | Indexer.prototype.addSection = function(path, section) { 42 | var url = [path, section.id].join('#'); 43 | 44 | var title = this.text( 45 | _.filter(section, {'type': 'heading'}) 46 | ); 47 | 48 | var body = this.text( 49 | _.omit(section, {'type': 'heading'}) 50 | ); 51 | 52 | // Add to lunr index 53 | this.idx.add({ 54 | url: url, 55 | title: title, 56 | body: body, 57 | }); 58 | }; 59 | 60 | Indexer.prototype.add = function(lexedPage, url) { 61 | var sections = lexedPage; 62 | 63 | _.map(sections, _.partial(this.addSection, url)); 64 | }; 65 | 66 | Indexer.prototype.dump = function() { 67 | return JSON.stringify(this.idx); 68 | }; 69 | 70 | // Exports 71 | module.exports = Indexer; 72 | -------------------------------------------------------------------------------- /lib/generate/template.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var swig = require('swig'); 3 | var hljs = require('highlight.js'); 4 | 5 | var links = require('../utils/').links; 6 | var pkg = require('../../package.json'); 7 | 8 | swig.setDefaults({ 9 | locals: { 10 | gitbook: { 11 | version: pkg.version 12 | } 13 | } 14 | }); 15 | 16 | // Swig filter for returning the count of lines in a code section 17 | swig.setFilter('lines', function(content) { 18 | return content.split('\n').length; 19 | }); 20 | 21 | // Swig filter for returning a link to the associated html file of a markdown file 22 | swig.setFilter('mdLink', function(link, level) { 23 | var link = link.replace(".md", ".html"); 24 | if (link == "README.html") link = "index.html"; 25 | if (level && level == 0) link = "index.html"; 26 | return link; 27 | }); 28 | 29 | // Swig filter: highlight coloration 30 | swig.setFilter('code', function(code, lang) { 31 | try { 32 | return hljs.highlight(lang, code).value; 33 | } catch(e) { 34 | return hljs.highlightAuto(code).value; 35 | } 36 | }); 37 | 38 | // Convert a level into a deep level 39 | swig.setFilter('lvl', function(lvl) { 40 | return lvl.split(".").length; 41 | }); 42 | 43 | // Join path 44 | swig.setFilter('pathJoin', function(base, _path) { 45 | return links.join(base, _path); 46 | }); 47 | 48 | // Is a link an absolute link 49 | swig.setFilter('isExternalLink', function(link) { 50 | return links.isExternal(link); 51 | }); 52 | 53 | module.exports = swig; 54 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parse: require('./parse/'), 3 | generate: require('./generate/') 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /lib/parse/glossary.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var kramed = require('kramed'); 3 | 4 | // Get all the pairs of header + paragraph in a list of nodes 5 | function groups(nodes) { 6 | // A list of next nodes 7 | var next = nodes.slice(1).concat(null); 8 | 9 | return _.reduce(nodes, function(accu, node, idx) { 10 | // Skip 11 | if(!( 12 | node.type === 'heading' && 13 | (next[idx] && next[idx].type === 'paragraph') 14 | )) { 15 | return accu; 16 | } 17 | 18 | // Add group 19 | accu.push([ 20 | node, 21 | next[idx] 22 | ]); 23 | 24 | return accu; 25 | }, []); 26 | } 27 | 28 | function parseGlossary(src) { 29 | var nodes = kramed.lexer(src); 30 | 31 | return groups(nodes) 32 | .map(function(pair) { 33 | // Simplify each group to a simple object with name/description 34 | return { 35 | name: pair[0].text, 36 | id: entryId(pair[0].text), 37 | description: pair[1].text, 38 | }; 39 | }); 40 | } 41 | 42 | // Normalizes a glossary entry's name to create an ID 43 | function entryId(name) { 44 | return name.toLowerCase(); 45 | } 46 | 47 | module.exports = parseGlossary; 48 | module.exports.entryId = entryId; 49 | -------------------------------------------------------------------------------- /lib/parse/include.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function(markdown, includer) { 4 | // Memoized include function (to cache lookups) 5 | var _include = _.memoize(includer); 6 | 7 | return markdown.replace(/{{([\s\S]+?)}}/g, function(match, key) { 8 | // If fails leave content as is 9 | key = key.trim(); 10 | return _include(key) || match; 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/parse/includer.js: -------------------------------------------------------------------------------- 1 | // Return a fs inclduer 2 | module.exports = function(ctx, folders, resolveFile, readFile) { 3 | return function(name) { 4 | return ctx[name] || 5 | folders.map(function(folder) { 6 | // Try including snippet from FS 7 | try { 8 | var fname = resolveFile(folder, name); 9 | // Trim trailing newlines/space of imported snippets 10 | return readFile(fname, 'utf8').trimRight(); 11 | } catch(err) {} 12 | }) 13 | .filter(Boolean)[0]; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/parse/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | summary: require('./summary'), 3 | glossary: require('./glossary'), 4 | langs: require('./langs'), 5 | page: require('./page'), 6 | lex: require('./lex'), 7 | progress: require('./progress'), 8 | navigation: require('./navigation'), 9 | readme: require('./readme'), 10 | includer: require('./includer') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/parse/is_exercise.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function isExercise(nodes) { 4 | var codeType = { type: 'code' }; 5 | 6 | // Number of code nodes in section 7 | var len = _.filter(nodes, codeType).length; 8 | 9 | return ( 10 | // Got 3 or 4 code blocks 11 | (len === 3 || len === 4) && 12 | // Ensure all nodes are at the end 13 | _.all(_.last(nodes, len), codeType) 14 | ); 15 | } 16 | 17 | module.exports = isExercise; 18 | -------------------------------------------------------------------------------- /lib/parse/is_quiz.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function isQuizNode(node) { 4 | return (/^[(\[][ x][)\]]/).test(node.text || node); 5 | } 6 | 7 | function isTableQuestion(nodes) { 8 | var block = questionBlock(nodes); 9 | return ( 10 | block.length === 1 && 11 | block[0].type === 'table' && 12 | _.all(block[0].cells[0].slice(1), isQuizNode) 13 | ); 14 | } 15 | 16 | function isListQuestion(nodes) { 17 | var block = questionBlock(nodes); 18 | // Counter of when we go in and out of lists 19 | var inlist = 0; 20 | // Number of lists we found 21 | var lists = 0; 22 | // Elements found outside a list 23 | var outsiders = 0; 24 | // Ensure that we have nothing except lists 25 | _.each(block, function(node) { 26 | if(node.type === 'list_start') { 27 | inlist++; 28 | } else if(node.type === 'list_end') { 29 | inlist--; 30 | lists++; 31 | } else if(inlist === 0) { 32 | // Found non list_start or list_end whilst outside a list 33 | outsiders++; 34 | } 35 | }); 36 | return lists > 0 && outsiders === 0; 37 | } 38 | 39 | function isQuestion(nodes) { 40 | return isListQuestion(nodes) || isTableQuestion(nodes); 41 | } 42 | 43 | // Remove (optional) paragraph header node and blockquote 44 | function questionBlock(nodes) { 45 | return nodes.slice( 46 | nodes[0].type === 'paragraph' ? 1 : 0, 47 | _.findIndex(nodes, { type: 'blockquote_start' }) 48 | ); 49 | } 50 | 51 | function splitQuestions(nodes) { 52 | // Represents nodes in current question 53 | var buffer = []; 54 | return _.reduce(nodes, function(accu, node) { 55 | // Add node to buffer 56 | buffer.push(node); 57 | 58 | // Flush buffer once we hit the end of a question 59 | if(node.type === 'blockquote_end') { 60 | accu.push(buffer); 61 | // Clear buffer 62 | buffer = []; 63 | } 64 | 65 | return accu; 66 | }, []); 67 | } 68 | 69 | function isQuiz(nodes) { 70 | // Extract potential questions 71 | var questions = splitQuestions( 72 | // Skip quiz title if there 73 | nodes.slice( 74 | (nodes[0] && nodes[0].type) === 'paragraph' ? 1 : 0 75 | ) 76 | ); 77 | 78 | // Nothing that looks like questions 79 | if(questions.length === 0) { 80 | return false; 81 | } 82 | 83 | // Ensure all questions are correctly structured 84 | return _.all(questions, isQuestion); 85 | } 86 | 87 | module.exports = isQuiz; 88 | -------------------------------------------------------------------------------- /lib/parse/langs.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var parseEntries = require("./summary").entries; 3 | 4 | 5 | var parseLangs = function(content) { 6 | var entries = parseEntries(content); 7 | 8 | return { 9 | list: _.chain(entries) 10 | .filter(function(entry) { 11 | return Boolean(entry.path); 12 | }) 13 | .map(function(entry) { 14 | return { 15 | title: entry.title, 16 | path: entry.path, 17 | lang: entry.path.replace("/", "") 18 | }; 19 | }) 20 | .value() 21 | }; 22 | }; 23 | 24 | 25 | module.exports = parseLangs; -------------------------------------------------------------------------------- /lib/parse/lex.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var kramed = require('kramed'); 3 | 4 | var isExercise = require('./is_exercise'); 5 | var isQuiz = require('./is_quiz'); 6 | 7 | // Split a page up into sections (lesson, exercises, ...) 8 | function splitSections(nodes) { 9 | var section = []; 10 | 11 | return _.reduce(nodes, function(sections, el) { 12 | if(el.type === 'hr') { 13 | sections.push(section); 14 | section = []; 15 | } else { 16 | section.push(el); 17 | } 18 | 19 | return sections; 20 | }, []).concat([section]); // Add remaining nodes 21 | } 22 | 23 | // What is the type of this section 24 | function sectionType(nodes, idx) { 25 | if(isExercise(nodes)) { 26 | return 'exercise'; 27 | } else if(isQuiz(nodes)) { 28 | return 'quiz'; 29 | } 30 | 31 | return 'normal'; 32 | } 33 | 34 | // Generate a uniqueId to identify this section in our code 35 | function sectionId(section, idx) { 36 | return _.uniqueId('gitbook_'); 37 | } 38 | 39 | function lexPage(src) { 40 | // Lex file 41 | var nodes = kramed.lexer(src); 42 | 43 | return _.chain(splitSections(nodes)) 44 | .map(function(section, idx) { 45 | // Detect section type 46 | section.type = sectionType(section, idx); 47 | return section; 48 | }) 49 | .map(function(section, idx) { 50 | // Give each section an ID 51 | section.id = sectionId(section, idx); 52 | return section; 53 | 54 | }) 55 | .filter(function(section) { 56 | return !_.isEmpty(section); 57 | }) 58 | .reduce(function(sections, section) { 59 | var last = _.last(sections); 60 | 61 | // Merge normal sections together 62 | if(last && last.type === section.type && last.type === 'normal') { 63 | last.push.apply(last, [{'type': 'hr'}].concat(section)); 64 | } else { 65 | // Add to list of sections 66 | sections.push(section); 67 | } 68 | 69 | return sections; 70 | }, []) 71 | .map(function(section) { 72 | section.links = nodes.links; 73 | return section; 74 | }) 75 | .value(); 76 | } 77 | 78 | // Exports 79 | module.exports = lexPage; 80 | -------------------------------------------------------------------------------- /lib/parse/navigation.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | // Cleans up an article/chapter object 4 | // remove 'articles' attributes 5 | function clean(obj) { 6 | return obj && _.omit(obj, ['articles']); 7 | } 8 | 9 | function flattenChapters(chapters) { 10 | return _.reduce(chapters, function(accu, chapter) { 11 | return accu.concat([clean(chapter)].concat(flattenChapters(chapter.articles))); 12 | }, []); 13 | } 14 | 15 | // Returns from a summary a map of 16 | /* 17 | { 18 | "file/path.md": { 19 | prev: ..., 20 | next: ..., 21 | }, 22 | ... 23 | } 24 | */ 25 | function navigation(summary, files) { 26 | // Support single files as well as list 27 | files = _.isArray(files) ? files : (_.isString(files) ? [files] : null); 28 | 29 | // List of all navNodes 30 | // Flatten chapters, then add in default README node if ndeeded etc ... 31 | var navNodes = flattenChapters(summary.chapters); 32 | var prevNodes = [null].concat(navNodes.slice(0, -1)); 33 | var nextNodes = navNodes.slice(1).concat([null]); 34 | 35 | // Mapping of prev/next for a give path 36 | var mapping = _.chain(_.zip(navNodes, prevNodes, nextNodes)) 37 | .map(function(nodes) { 38 | var current = nodes[0], prev = nodes[1], next = nodes[2]; 39 | 40 | // Skip if no path 41 | if(!current.path) return null; 42 | 43 | return [current.path, { 44 | title: current.title, 45 | prev: prev, 46 | next: next, 47 | level: current.level, 48 | }]; 49 | }) 50 | .filter() 51 | .object() 52 | .value(); 53 | 54 | // Filter for only files we want 55 | if(files) { 56 | return _.pick(mapping, files); 57 | } 58 | 59 | return mapping; 60 | } 61 | 62 | 63 | // Exports 64 | module.exports = navigation; 65 | -------------------------------------------------------------------------------- /lib/parse/page.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var kramed = require('kramed'); 3 | var hljs = require('highlight.js'); 4 | 5 | var lex = require('./lex'); 6 | var renderer = require('./renderer'); 7 | 8 | var include = require('./include'); 9 | var lnormalize = require('../utils/lang').normalize; 10 | 11 | 12 | 13 | // Render a section using our custom renderer 14 | function render(section, _options) { 15 | // Copy section 16 | var links = section.links || {}; 17 | section = _.toArray(section); 18 | section.links = links; 19 | 20 | // Build options using defaults and our custom renderer 21 | var options = _.extend({}, kramed.defaults, { 22 | renderer: renderer(null, _options), 23 | 24 | // Synchronous highlighting with highlight.js 25 | highlight: function (code, lang) { 26 | if(!lang) return code; 27 | 28 | // Normalize lang 29 | lang = lnormalize(lang); 30 | 31 | try { 32 | return hljs.highlight(lang, code).value; 33 | } catch(e) { } 34 | 35 | return code; 36 | } 37 | }); 38 | 39 | return kramed.parser(section, options); 40 | } 41 | 42 | function quizQuestion(node) { 43 | if (node.text) { 44 | node.text = node.text.replace(/^([\[(])x([\])])/, "$1 $2"); 45 | } else { 46 | return node.replace(/^([\[(])x([\])])/, "$1 $2"); 47 | } 48 | } 49 | 50 | function parsePage(src, options) { 51 | options = options || {}; 52 | 53 | // Lex if not already lexed 54 | var parsed = { 55 | lexed: (_.isArray(src) ? src : lex(include(src, options.includer || function() { return undefined; }))) 56 | }; 57 | parsed.sections = parsed.lexed.map(function(section) { 58 | // Transform given type 59 | if(section.type === 'exercise') { 60 | var nonCodeNodes = _.reject(section, { 61 | 'type': 'code' 62 | }); 63 | 64 | var codeNodes = _.filter(section, { 65 | 'type': 'code' 66 | }); 67 | 68 | // Languages in code blocks 69 | var langs = _.pluck(codeNodes, 'lang').map(lnormalize); 70 | 71 | // Check that they are all the same 72 | var validLangs = _.all(_.map(langs, function(lang) { 73 | return lang && lang === langs[0]; 74 | })); 75 | 76 | // Main language 77 | var lang = validLangs ? langs[0] : null; 78 | 79 | return { 80 | id: section.id, 81 | type: section.type, 82 | content: render(nonCodeNodes, options), 83 | lang: lang, 84 | code: { 85 | base: codeNodes[0].text, 86 | solution: codeNodes[1].text, 87 | validation: codeNodes[2].text, 88 | // Context is optional 89 | context: codeNodes[3] ? codeNodes[3].text : null, 90 | } 91 | }; 92 | } else if (section.type === 'quiz') { 93 | var quiz = [], question, foundFeedback = false; 94 | var nonQuizNodes = section[0].type === 'paragraph' && section[1].type !== 'list_start' ? [section[0]] : []; 95 | var quizNodes = section.slice(0); 96 | quizNodes.splice(0, nonQuizNodes.length); 97 | 98 | for (var i = 0; i < quizNodes.length; i++) { 99 | var node = quizNodes[i]; 100 | 101 | if (question && (((node.type === 'list_end' || node.type === 'blockquote_end') && i === quizNodes.length - 1) 102 | || node.type === 'table' || (node.type === 'paragraph' && !foundFeedback))) { 103 | quiz.push({ 104 | base: render(question.questionNodes, options), 105 | solution: render(question.solutionNodes, options), 106 | feedback: render(question.feedbackNodes, options) 107 | }); 108 | } 109 | 110 | if (node.type === 'table' || (node.type === 'paragraph' && !foundFeedback)) { 111 | question = { questionNodes: [], solutionNodes: [], feedbackNodes: [] }; 112 | } 113 | 114 | if (node.type === 'blockquote_start') { 115 | foundFeedback = true; 116 | } else if (node.type === 'blockquote_end') { 117 | foundFeedback = false; 118 | } 119 | 120 | if (node.type === 'table') { 121 | question.solutionNodes.push(_.cloneDeep(node)); 122 | node.cells = node.cells.map(function(row) { 123 | return row.map(quizQuestion); 124 | }); 125 | question.questionNodes.push(node); 126 | } else if (!/blockquote/.test(node.type)) { 127 | if (foundFeedback) { 128 | question.feedbackNodes.push(node); 129 | } else if (node.type === 'paragraph' || node.type === 'text'){ 130 | question.solutionNodes.push(_.cloneDeep(node)); 131 | quizQuestion(node); 132 | question.questionNodes.push(node); 133 | } else { 134 | question.solutionNodes.push(node); 135 | question.questionNodes.push(node); 136 | } 137 | } 138 | } 139 | 140 | return { 141 | id: section.id, 142 | type: section.type, 143 | content: render(nonQuizNodes, options), 144 | quiz: quiz 145 | }; 146 | } 147 | 148 | // Render normal pages 149 | return { 150 | id: section.id, 151 | type: section.type, 152 | content: render(section, options) 153 | }; 154 | }); 155 | 156 | return parsed; 157 | } 158 | 159 | // Exports 160 | module.exports = parsePage; 161 | -------------------------------------------------------------------------------- /lib/parse/progress.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | // Returns from a navigation and a current file, a snapshot of current detailed state 4 | var calculProgress = function(navigation, current) { 5 | var n = _.size(navigation); 6 | var percent = 0, prevPercent = 0, currentChapter = null; 7 | var done = true; 8 | 9 | var chapters = _.chain(navigation) 10 | .map(function(nav, path) { 11 | nav.path = path; 12 | return nav; 13 | }) 14 | .map(function(nav, i) { 15 | // Calcul percent 16 | nav.percent = (i * 100) / Math.max((n - 1), 1); 17 | 18 | // Is it done 19 | nav.done = done; 20 | if (nav.path == current) { 21 | currentChapter = nav; 22 | percent = nav.percent; 23 | done = false; 24 | } else if (done) { 25 | prevPercent = nav.percent; 26 | } 27 | 28 | return nav; 29 | }) 30 | .value(); 31 | 32 | return { 33 | // Previous percent 34 | prevPercent: prevPercent, 35 | 36 | // Current percent 37 | percent: percent, 38 | 39 | // List of chapter with progress 40 | chapters: chapters, 41 | 42 | // Current chapter 43 | current: currentChapter 44 | }; 45 | } 46 | 47 | module.exports = calculProgress; 48 | -------------------------------------------------------------------------------- /lib/parse/readme.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var kramed = require('kramed'); 3 | var textRenderer = require('kramed-text-renderer'); 4 | 5 | function extractFirstNode(nodes, nType) { 6 | return _.chain(nodes) 7 | .filter(function(node) { 8 | return node.type == nType; 9 | }) 10 | .pluck("text") 11 | .first() 12 | .value(); 13 | } 14 | 15 | 16 | function parseReadme(src) { 17 | var nodes, title, description; 18 | var renderer = textRenderer(); 19 | 20 | // Parse content 21 | nodes = kramed.lexer(src); 22 | 23 | title = extractFirstNode(nodes, "heading") || ''; 24 | description = extractFirstNode(nodes, "paragraph") || ''; 25 | 26 | var convert = _.compose( 27 | function(text) { 28 | return _.unescape(text.replace(/(\r\n|\n|\r)/gm, "")); 29 | }, 30 | function(text) { 31 | return kramed.parse(text, _.extend({}, kramed.defaults, { 32 | renderer: renderer 33 | })); 34 | } 35 | ); 36 | 37 | return { 38 | title: convert(title), 39 | description: convert(description) 40 | }; 41 | } 42 | 43 | 44 | // Exports 45 | module.exports = parseReadme; 46 | -------------------------------------------------------------------------------- /lib/parse/renderer.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var _ = require('lodash'); 3 | var inherits = require('util').inherits; 4 | var links = require('../utils').links; 5 | var kramed = require('kramed'); 6 | 7 | var rendererId = 0; 8 | 9 | function GitBookRenderer(options, extra_options) { 10 | if(!(this instanceof GitBookRenderer)) { 11 | return new GitBookRenderer(options, extra_options); 12 | } 13 | GitBookRenderer.super_.call(this, options); 14 | 15 | this._extra_options = extra_options; 16 | this.quizRowId = 0; 17 | this.id = rendererId++; 18 | this.quizIndex = 0; 19 | } 20 | inherits(GitBookRenderer, kramed.Renderer); 21 | 22 | GitBookRenderer.prototype._unsanitized = function(href) { 23 | var prot = ''; 24 | try { 25 | prot = decodeURIComponent(unescape(href)) 26 | .replace(/[^\w:]/g, '') 27 | .toLowerCase(); 28 | 29 | } catch (e) { 30 | return true; 31 | } 32 | 33 | if(prot.indexOf('javascript:') === 0) { 34 | return true; 35 | } 36 | 37 | return false; 38 | }; 39 | 40 | GitBookRenderer.prototype.link = function(href, title, text) { 41 | // Our "fixed" href 42 | var _href = href; 43 | 44 | // Don't build if it looks malicious 45 | if (this.options.sanitize && this._unsanitized(href)) { 46 | return text; 47 | } 48 | 49 | // Parsed version of the url 50 | var parsed = url.parse(href); 51 | var o = this._extra_options; 52 | var extname = parsed.path? _.last(parsed.path.split(".")) : ""; 53 | 54 | // Relative link, rewrite it to point to github repo 55 | if(links.isRelative(_href) && extname == "md") { 56 | _href = links.toAbsolute(_href, o.dir || "./", o.outdir || "./"); 57 | _href = _href.replace(".md", ".html"); 58 | } 59 | 60 | // Generate HTML for link 61 | var out = ''; 71 | return out; 72 | }; 73 | 74 | GitBookRenderer.prototype.image = function(href, title, text) { 75 | // Our "fixed" href 76 | var _href = href; 77 | 78 | // Parsed version of the url 79 | var parsed = url.parse(href); 80 | 81 | // Options 82 | var o = this._extra_options; 83 | 84 | // Relative image, rewrite it depending output 85 | if(links.isRelative(href) && o && o.dir && o.outdir) { 86 | // o.dir: directory parent of the file currently in rendering process 87 | // o.outdir: directory parent from the html output 88 | 89 | _href = links.toAbsolute(_href, o.dir, o.outdir); 90 | } 91 | 92 | return GitBookRenderer.super_.prototype.image.call(this, _href, title, text); 93 | }; 94 | 95 | GitBookRenderer.prototype.tablerow = function(content) { 96 | this.quizRowId += 1; 97 | return GitBookRenderer.super_.prototype.tablerow(content); 98 | }; 99 | 100 | var fieldRegex = /^([(\[])([ x])[\])]/; 101 | GitBookRenderer.prototype._createCheckboxAndRadios = function(text) { 102 | var match = fieldRegex.exec(text); 103 | if (!match) { 104 | return text; 105 | } 106 | //fix radio input uncheck failed 107 | var quizFieldName='quiz-row-' + this.id + '-' + this.quizRowId ; 108 | var quizIdentifier = quizFieldName + '-' + this.quizIndex++; 109 | var field = "" : "'/>"; 112 | var splittedText = text.split(fieldRegex); 113 | var length = splittedText.length; 114 | var label = ''; 115 | return text.replace(fieldRegex, field).replace(splittedText[length - 1], label); 116 | }; 117 | 118 | GitBookRenderer.prototype.tablecell = function(content, flags) { 119 | return GitBookRenderer.super_.prototype.tablecell(this._createCheckboxAndRadios(content), flags); 120 | }; 121 | 122 | GitBookRenderer.prototype.listitem = function(text) { 123 | return GitBookRenderer.super_.prototype.listitem(this._createCheckboxAndRadios(text)); 124 | }; 125 | 126 | GitBookRenderer.prototype.code = function(code, lang, escaped) { 127 | return GitBookRenderer.super_.prototype.code.call( 128 | this, 129 | code, 130 | lang, 131 | escaped 132 | ); 133 | }; 134 | 135 | GitBookRenderer.prototype.heading = function(text, level, raw) { 136 | var id = this.options.headerPrefix + raw.toLowerCase().replace(/[^\w -]+/g, '').replace(/ /g, '-'); 137 | return '' + text + '\n'; 138 | }; 139 | 140 | // Exports 141 | module.exports = GitBookRenderer; 142 | -------------------------------------------------------------------------------- /lib/parse/summary.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var kramed = require('kramed'); 3 | 4 | 5 | // Utility function for splitting a list into groups 6 | function splitBy(list, starter, ender) { 7 | var starts = 0; 8 | var ends = 0; 9 | var group = []; 10 | 11 | // Groups 12 | return _.reduce(list, function(groups, value) { 13 | // Ignore start and end delimiters in resulted groups 14 | if(starter(value)) { 15 | starts++; 16 | } else if(ender(value)) { 17 | ends++; 18 | } 19 | 20 | // Add current value to group 21 | group.push(value); 22 | 23 | // We've got a matching 24 | if(starts === ends && starts !== 0) { 25 | // Add group to end groups 26 | // (remove starter and ender token) 27 | groups.push(group.slice(1, -1)); 28 | 29 | // Reset group 30 | group = []; 31 | } 32 | 33 | return groups; 34 | }, []); 35 | } 36 | 37 | function listSplit(nodes, start_type, end_type) { 38 | return splitBy(nodes, function(el) { 39 | return el.type === start_type; 40 | }, function(el) { 41 | return el.type === end_type; 42 | }); 43 | } 44 | 45 | // Get the biggest list 46 | // out of a list of kramed nodes 47 | function filterList(nodes) { 48 | return _.chain(nodes) 49 | .toArray() 50 | .rest(function(el) { 51 | // Get everything after list_start 52 | return el.type !== 'list_start'; 53 | }) 54 | .reverse() 55 | .rest(function(el) { 56 | // Get everything after list_end (remember we're reversed) 57 | return el.type !== 'list_end'; 58 | }) 59 | .reverse() 60 | .value().slice(1, -1); 61 | } 62 | 63 | function skipSpace(nodes) { 64 | return _.filter(nodes, function(node) { 65 | return node && node.type != 'space'; 66 | }); 67 | } 68 | 69 | function correctLoose(nodes) { 70 | return _.map(nodes, function(node) { 71 | // Return normal nodes 72 | if(!node || node.type != 'loose_item_start') { 73 | return node 74 | } 75 | 76 | // Correct loose items 77 | node.type = 'list_item_start'; 78 | 79 | return node; 80 | }) 81 | } 82 | 83 | // Parses an Article or Chapter title 84 | // supports extracting links 85 | function parseTitle(src, nums) { 86 | // Check if it's a link 87 | var matches = kramed.InlineLexer.rules.link.exec(src); 88 | 89 | var level = nums.join('.'); 90 | 91 | // Not a link, return plain text 92 | if(!matches) { 93 | return { 94 | title: src, 95 | level: level, 96 | path: null, 97 | }; 98 | } 99 | 100 | return { 101 | title: matches[1], 102 | level: level, 103 | 104 | // Normalize path 105 | // 1. Convert Window's "\" to "/" 106 | // 2. Remove leading "/" if exists 107 | path: matches[2].replace(/\\/g, '/').replace(/^\/+/, ''), 108 | }; 109 | } 110 | 111 | function parseChapter(nodes, nums) { 112 | // Convert single number to an array 113 | nums = _.isArray(nums) ? nums : [nums]; 114 | 115 | return _.extend(parseTitle(_.first(nodes).text, nums), { 116 | articles: _.map(listSplit(filterList(nodes), 'list_item_start', 'list_item_end'), function(nodes, i) { 117 | return parseChapter(nodes, nums.concat(i + 1)); 118 | }) 119 | }); 120 | } 121 | 122 | function defaultChapterList(chapterList, options) { 123 | var first = _.first(chapterList); 124 | 125 | // Check if introduction node was specified in SUMMARY.md 126 | if (first) { 127 | var chapter = parseChapter(first, [0]); 128 | 129 | // Already have README node, we're good to go 130 | if(chapter.path === 'README.md') { 131 | return chapterList; 132 | } 133 | } 134 | 135 | // Check if introduction node was specified in book.json 136 | if (options.introduction) { 137 | var intro = options.introduction; 138 | return [ 139 | [ { type: 'text', text: '[' + intro.title + '](' + intro.path + ')' } ] 140 | ].concat(chapterList); 141 | } 142 | 143 | // It wasn't specified, use the first one 144 | return chapterList; 145 | } 146 | 147 | function listGroups(src) { 148 | var nodes = kramed.lexer(src); 149 | 150 | // Get out groups of lists 151 | return listSplit( 152 | filterList(correctLoose(skipSpace(nodes))), 153 | 'list_item_start', 'list_item_end' 154 | ); 155 | } 156 | 157 | function parseSummary(src, options) { 158 | // Split out chapter sections 159 | var chapters = defaultChapterList(listGroups(src), options); 160 | 161 | return { 162 | chapters: chapters.map(parseChapter) 163 | }; 164 | } 165 | 166 | function parseEntries (src) { 167 | return listGroups(src).map(parseChapter); 168 | } 169 | 170 | // Exports 171 | module.exports = parseSummary; 172 | module.exports.entries = parseEntries; 173 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lang: require('./lang'), 3 | links: require('./links') 4 | }; 5 | -------------------------------------------------------------------------------- /lib/utils/lang.js: -------------------------------------------------------------------------------- 1 | var MAP = { 2 | 'py': 'python', 3 | 'js': 'javascript', 4 | 'rb': 'ruby', 5 | 'csharp': 'cs', 6 | }; 7 | 8 | function normalize(lang) { 9 | if(!lang) { return null; } 10 | 11 | var lower = lang.toLowerCase(); 12 | return MAP[lower] || lower; 13 | } 14 | 15 | // Exports 16 | module.exports = { 17 | normalize: normalize, 18 | MAP: MAP 19 | }; 20 | -------------------------------------------------------------------------------- /lib/utils/links.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var path = require('path'); 3 | 4 | // Is the link an external link 5 | var isExternal = function(href) { 6 | try { 7 | return Boolean(url.parse(href).protocol); 8 | } catch(err) { } 9 | 10 | return false; 11 | }; 12 | 13 | // Return true if the link is relative 14 | var isRelative = function(href) { 15 | try { 16 | var parsed = url.parse(href); 17 | 18 | return !parsed.protocol && parsed.path && parsed.path[0] != '/'; 19 | } catch(err) {} 20 | 21 | return true; 22 | }; 23 | 24 | // Relative to absolute path 25 | // dir: directory parent of the file currently in rendering process 26 | // outdir: directory parent from the html output 27 | 28 | var toAbsolute = function(_href, dir, outdir) { 29 | // Absolute file in source 30 | _href = path.join(dir, _href); 31 | 32 | // make it relative to output 33 | _href = path.relative(outdir, _href); 34 | 35 | if (process.platform === 'win32') { 36 | _href = _href.replace(/\\/g, '/'); 37 | } 38 | 39 | return _href; 40 | }; 41 | 42 | // Join links 43 | 44 | var join = function() { 45 | var _href = path.join.apply(path, arguments); 46 | 47 | if (process.platform === 'win32') { 48 | _href = _href.replace(/\\/g, '/'); 49 | } 50 | 51 | return _href; 52 | }; 53 | 54 | 55 | module.exports = { 56 | isRelative: isRelative, 57 | isExternal: isExternal, 58 | toAbsolute: toAbsolute, 59 | join: join 60 | }; 61 | -------------------------------------------------------------------------------- /lib/utils/string.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | function escapeShellArg(arg) { 4 | var ret = ''; 5 | 6 | ret = arg.replace(/"/g, '\\"'); 7 | 8 | return "\"" + ret + "\""; 9 | } 10 | 11 | function optionsToShellArgs(options) { 12 | return _.chain(options) 13 | .map(function(value, key) { 14 | if (value == null || value === false) return null; 15 | if (value === true) return key; 16 | return key+"="+escapeShellArg(value); 17 | }) 18 | .compact() 19 | .value() 20 | .join(" "); 21 | } 22 | 23 | module.exports = { 24 | escapeShellArg: escapeShellArg, 25 | optionsToShellArgs: optionsToShellArgs 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitbook", 3 | "version": "1.5.0", 4 | "homepage": "https://www.gitbook.com", 5 | "description": "Library and cmd utility to generate GitBooks", 6 | "main": "lib/index.js", 7 | "dependencies": { 8 | "q": "1.0.1", 9 | "lodash": "2.4.1", 10 | "kramed": "0.4.4", 11 | "kramed-text-renderer": "0.2.1", 12 | "lunr": "codepiano/lunr.js", 13 | "swig": "1.3.2", 14 | "send": "0.2.0", 15 | "fstream-ignore": "0.0.7", 16 | "commander": "2.2.0", 17 | "graceful-fs": "3.0.2", 18 | "fs-extra": "0.10.0", 19 | "highlight.js": "8.3.0", 20 | "tmp": "0.0.23", 21 | "semver": "2.2.1", 22 | "npmi": "0.1.1", 23 | "gaze": "~0.5.1", 24 | "resolve": "0.6.3", 25 | "tiny-lr-fork": "0.0.5", 26 | "gitbook-plugin": "0.0.2", 27 | "gitbook-plugin-mathjax": "0.0.6", 28 | "gitbook-plugin-livereload": "0.0.1", 29 | "gitbook-plugin-exercises": "1.0.0", 30 | "gitbook-plugin-quizzes": "1.0.0" 31 | }, 32 | "devDependencies": { 33 | "mocha": "1.18.2", 34 | "grunt": "~0.4.2", 35 | "grunt-cli": "0.1.11", 36 | "grunt-contrib-copy": "0.5.0", 37 | "grunt-contrib-less": "~0.5.0", 38 | "grunt-contrib-requirejs": "0.4.1", 39 | "grunt-bower-install-simple": "0.9.2", 40 | "grunt-browserify": "3.1.0", 41 | "grunt-contrib-uglify": "0.6.0" 42 | }, 43 | "scripts": { 44 | "test": "export TESTING=true; mocha --reporter list" 45 | }, 46 | "bin": { 47 | "gitbook": "./bin/gitbook.js" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/GitbookIO/gitbook.git" 52 | }, 53 | "keywords": [ 54 | "git", 55 | "book", 56 | "gitbook" 57 | ], 58 | "author": "FriendCode Inc. ", 59 | "license": "Apache 2", 60 | "bugs": { 61 | "url": "https://github.com/GitbookIO/gitbook/issues" 62 | }, 63 | "contributors": [ 64 | { 65 | "name": "Aaron O'Mullan", 66 | "email": "aaron@gitbook.com" 67 | }, 68 | { 69 | "name": "Samy Pessé", 70 | "email": "samy@gitbook.com" 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/preview.png -------------------------------------------------------------------------------- /test/bin/lex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | 5 | var gitbook = require('../../'); 6 | 7 | if(process.argv < 3) { 8 | console.error('Please specify a filename'); 9 | process.exit(1); 10 | } 11 | 12 | var content = fs.readFileSync(process.argv[2], 'utf8'); 13 | 14 | var lexed = gitbook.parse.lex(content); 15 | 16 | console.log(JSON.stringify(lexed, null, 2)); 17 | -------------------------------------------------------------------------------- /test/bin/summary.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | 5 | var gitbook = require('../../'); 6 | 7 | if(process.argv < 3) { 8 | console.error('Please specify a filename'); 9 | process.exit(1); 10 | } 11 | 12 | var content = fs.readFileSync(process.argv[2], 'utf8'); 13 | 14 | var lexed = gitbook.parse.summary(content); 15 | 16 | console.log(JSON.stringify(lexed, null, 2)); 17 | -------------------------------------------------------------------------------- /test/fixtures/ALTERNATIVE_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Custom name for Introduction](README.md) 4 | * [Chapter 1](chapter-1/README.md) 5 | * [Article 1](chapter-1/ARTICLE1.md) 6 | * [Article 2](chapter-1/ARTICLE2.md) 7 | * [article 1.2.1](chapter-1/ARTICLE-1-2-1.md) 8 | * [article 1.2.2](chapter-1/ARTICLE-1-2-2.md) 9 | * [Chapter 2](chapter-2/README.md) 10 | * [Chapter 3](chapter-3/README.md) 11 | * [Chapter 4](chapter-4/README.md) 12 | * Unfinished article 13 | * Unfinished Chapter 14 | -------------------------------------------------------------------------------- /test/fixtures/FALSE_QUIZ.md: -------------------------------------------------------------------------------- 1 | # Learn Node.js by Example 2 | 3 | 4 | ## Requirements 5 | 6 | - [x] A computer with internet access 7 | - [ ] Time: 30h (e.g. 2 months 30 mins per day or **1 week intensive**) 8 | 9 | 10 | ## What is Node.js ? 11 | 12 | Node.js lets you *easily* build networked software (websites, applications "apps", 13 | using JavaScript). 14 | 15 | Its not "*point-and-click*" like WordPress, SquareSpace or Salesforce; 16 | you will need to write some "code". But as I will demonstrate, that's 17 | a *lot* easier than it sounds and gives you more power/flexibility 18 | and puts you in full control. 19 | 20 | ## Node.js is not "Version 1.0" yet can I used it in Production? 21 | 22 | Yes! Some of the biggest organisations/companies in the world 23 | are using Node.js in Production systems: 24 | 25 | [Alibaba](https://github.com/alibaba/node-hbase-client), 26 | [Ajax.org](Ajax.org), 27 | [Box.com](http://tech.blog.box.com/2014/06/node-js-high-availability-at-box/), British Sky Broadcasting (Sky/Now TV), 28 | CNN, 29 | [Cloudup](https://cloudup.com/), 30 | Conde Nast, 31 | [DirectTV](http://strongloop.com/strongblog/node-summit-media-companies-embrace-node-js-for-rapidly-developing-responsive-apps/), 32 | [Dow Jones](http://nodejs.org/industry), 33 | eBay, 34 | [FeedHenry](http://www.feedhenry.com/), 35 | [GitHub](https://twitter.com/github/status/16979699217465344), 36 | [Google](http://venturebeat.com/2012/01/24/node-at-google-mozilla-yahoo/), 37 | [Groupon](http://nodeup.com/fiftyeight), 38 | HBO, 39 | Help.com, 40 | [HP](https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node), 41 | iTV, 42 | [Joyent](https://www.joyent.com/) (duh!), 43 | [Klout](https://klout.com), 44 | LinkedIn, 45 | McDonalds, 46 | [Medium](https://medium.com/the-story), 47 | Mozilla, 48 | NetFlix, 49 | [OpenTable](http://hapijs.com/community), 50 | PayPal, 51 | Pearson, 52 | ~~Q~~, 53 | [Revolt](http://revolt.tv/), 54 | [Square](https://modulus.io/companies-using-node), 55 | Tesco, 56 | ThomasCook, 57 | Trello, 58 | Uber, 59 | Voxer, 60 | Walmart, 61 | Wikimedia (in progress of moving to SOA with node!) 62 | Yahoo, 63 | Yammer, 64 | [Yandex](https://www.youtube.com/watch?v=zdCxgdH4wZo), 65 | [Zendesk](http://radar.zendesk.com/) 66 | 67 | Want more? See: http://nodejs.org/industry/ and
68 | https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node 69 | 70 | 71 | # Try it! 72 | 73 | ## Download & Install 74 | 75 | > http://nodejs.org/download/ 76 | 77 | 78 | ## Node.js (Core) API 79 | 80 | The node.js ("core") has many useful modules. 81 | 82 | Bookmark: [http://nodejs.org/api](http://nodejs.org/api/) (you will come back to it) 83 | 84 | 85 | 86 | ## Stability (Can we use it?) 87 | 88 | > *Which node.js* ***core*** *package(s) can/should I use?* 89 | 90 | Every core module has a 91 | ["***Stability Index***"](http://nodejs.org/api/documentation.html#documentation_stability_index) 92 | rating on the node.js API. 93 | 94 | **General rule**: If you are being *paid* to write code 95 | that runs in node.js,
pick core modules/methods 96 | with stability **Stable**, **API Frozen** and **Locked**. 97 | 98 | ![Node.js Stability Index](http://i.imgur.com/xIroFrS.png) 99 | 100 | 101 | ### Examples 102 | 103 | - [**cluster**](http://nodejs.org/api/cluster.html) is ***Experimental*** - don't use 104 | - [**domain**](http://nodejs.org/api/domain.html) is ***Unstable*** - don't use 105 | - [**path**](http://nodejs.org/api/path.html) is ***Stable*** - use 106 | - [**events**](http://nodejs.org/api/events.html) is ***Frozen*** - use 107 | - [**assert**](http://nodejs.org/api/assert.html) is ***Locked*** - use 108 | 109 | Core Modules to Learn 110 | 111 | - path 112 | - os 113 | 114 | 115 | 116 | Community Modules to Learn: 117 | 118 | - [jscs](https://www.npmjs.org/package/jscs) - code style checker 119 | - [q](https://www.npmjs.org/package/q) - promises library 120 | - [nd](https://www.npmjs.org/package/nd) - view documentation for a module 121 | -------------------------------------------------------------------------------- /test/fixtures/GITHUB_LINKS.md: -------------------------------------------------------------------------------- 1 | # Nice course 2 | 3 | Check out this source file [in C++](../src/something.cpp) 4 | -------------------------------------------------------------------------------- /test/fixtures/GLOSSARY.md: -------------------------------------------------------------------------------- 1 | # Magic 2 | Sufficiently advanced technology, beyond the understanding of the observer producing a sense of wonder. 3 | 4 | Hello, I am random noise in the middle of this beautiful Glossary. (Really astonishing !) 5 | 6 | # PHP 7 | An atrocious language, invented for the sole purpose of inflicting pain and suffering amongst the proframming wizards of this world. 8 | 9 | # Clojure 10 | Lisp re-invented for hipsters. 11 | 12 | # Go 13 | Go Go Google [Wow](https://www.google.com) 14 | 15 | Fantastic, I love code too ! : 16 | 17 | ```py 18 | 19 | def f(x): 20 | return x * 4 21 | 22 | # Wow this is some really awesome code 23 | # totally mind blowing 24 | # but we don't care, it shouldn't be in our glossary ! 25 | print(f(9)) 26 | ``` 27 | 28 | # Gitbook 29 | 30 | Awesome project. Really amazing, I'm really at a loss for words ... 31 | -------------------------------------------------------------------------------- /test/fixtures/HR_PAGE.md: -------------------------------------------------------------------------------- 1 | ## Wow such book 2 | 3 | Some nice content here 4 | 5 | --- 6 | 7 | A beautiful separator, but non an exercise or a quiz ! 8 | 9 | --- 10 | 11 | Some more beautiful text, because `this` book is awesome ... 12 | -------------------------------------------------------------------------------- /test/fixtures/IMAGES.md: -------------------------------------------------------------------------------- 1 | # Images Test 2 | 3 | This is a test for images path calculation. It supposed this fiel is in a syntax/ folder 4 | 5 | ### # 6 | 7 | [![Screen](./preview.png)](./preview.png) 8 | 9 | ### 2 10 | 11 | [![Screen](../preview2.png)](./preview2.png) 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/INCLUDES.md: -------------------------------------------------------------------------------- 1 | # Beautiful chapter 2 | 3 | Here is a nice included snippet : 4 | 5 | ```c 6 | {{ included.c }} 7 | ``` 8 | 9 | ---- 10 | 11 | An exercise using includes 12 | 13 | ```c 14 | {{ included.c }} 15 | 16 | Remove this extra code at the end 17 | ``` 18 | 19 | ```c 20 | {{ included.c }} 21 | ``` 22 | 23 | ```c 24 | {{ included.c }} 25 | 26 | This validation code is wrong but who cares ? 27 | ``` 28 | 29 | ---- 30 | -------------------------------------------------------------------------------- /test/fixtures/MARKDOWN_LINKS.md: -------------------------------------------------------------------------------- 1 | # Nice course 2 | 3 | Check out this other chapter [Test](test.md) 4 | 5 | Check out this other chapter [Test](../before.md) 6 | -------------------------------------------------------------------------------- /test/fixtures/PAGE.md: -------------------------------------------------------------------------------- 1 | # Python basics 2 | 3 | Python is a nice language, you can add stuff. Bla bla bla. 4 | 5 | Lets jump into an exercise : 6 | 7 | --- 8 | 9 | It's dead simple, `c` must be the sum of `a` and `b` 10 | 11 | ```py 12 | a = 1 13 | b = 2 14 | ``` 15 | 16 | ```py 17 | a = 1 18 | b = 2 19 | c = a + b 20 | ``` 21 | 22 | ```py 23 | assert(c, 3) 24 | ``` 25 | 26 | --- 27 | 28 | Some more nice content .... 29 | 30 | [Cool stuff](http://gitbook.com) 31 | 32 | [Link to another Markdown file](./xyz/file.md) 33 | 34 | And look at this pretty picture: 35 | ![Pretty](../assets/my-pretty-picture.png "Pretty") 36 | 37 | Lets go for another exercise but this time with some context : 38 | 39 | --- 40 | 41 | Exercise with some context code : 42 | 43 | Using the `double` function provided, build a `quadruple` function 44 | 45 | ```py 46 | 47 | ``` 48 | 49 | ```py 50 | 51 | def quadruple(x): 52 | return double(double(x)) 53 | 54 | ``` 55 | 56 | ```py 57 | assert(quadruple(8), 32) 58 | ``` 59 | 60 | ```py 61 | 62 | def double(x): 63 | return x * 2 64 | 65 | ``` 66 | 67 | --- 68 | 69 | -------------------------------------------------------------------------------- /test/fixtures/QUIZ_PAGE.md: -------------------------------------------------------------------------------- 1 | # Gitbook quiz 2 | 3 | Gitbook lets you write a quiz using GFM tables: 4 | 5 | --- 6 | 7 | Here's a quiz about Gitbook 8 | 9 | | | Good | Bad | 10 | | ---------------- | ---- | --- | 11 | | What is Gitbook? | (x) | ( ) | 12 | 13 | > Gitbook is good 14 | 15 | What does Gitbook support? 16 | - [x] Table-based questions with radio buttons 17 | - [x] Table-based questions with checkboxes 18 | - [ ] Telepathy 19 | - [x] List-based questions with checkboxes 20 | - [x] List-based questions with radio buttons 21 | - [ ] Moon-on-a-stick 22 | 23 | > Gitbook supports table and list based quiz questions using either radio buttons or checkboxes. 24 | > 25 | > Gitbook is not telepathic and does not give you the moon on a stick. 26 | 27 | --- 28 | 29 | Some more nice content .... 30 | 31 | [Cool stuff](http://gitbook.com) 32 | 33 | [Link to another Markdown file](./xyz/file.md) 34 | 35 | --- 36 | 37 | Quiz test 2: What does Gitbook support? 38 | - [x] Table-based questions with radio buttons 39 | - [x] Table-based questions with checkboxes 40 | - [ ] Telepathy 41 | - [x] List-based questions with checkboxes 42 | - [x] List-based questions with radio buttons 43 | - [ ] Moon-on-a-stick 44 | 45 | > Gitbook supports table and list based quiz questions using either radio buttons or checkboxes. 46 | 47 | --- 48 | 49 | -------------------------------------------------------------------------------- /test/fixtures/README.md: -------------------------------------------------------------------------------- 1 | # This is the title 2 | 3 | This is the book description. 4 | 5 | other content 6 | ... 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/SECTIONS.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | Some text 4 | 5 | --- 6 | 7 | ## NOT Exercise 8 | 9 | Simple subsection NOT exercise 10 | 11 | ``` 12 | x = 1 13 | ``` 14 | 15 | What is this 16 | 17 | ``` 18 | y = [1, 2, 3] 19 | ``` 20 | 21 | ``` 22 | z = {a: 1, b: 2} 23 | ``` 24 | 25 | --- 26 | 27 | ## Exercise 28 | 29 | Define a variable `x` equal to 10. 30 | 31 | ```js 32 | var x = 33 | ``` 34 | 35 | ```js 36 | var x = 10; 37 | ``` 38 | 39 | ```js 40 | assert(x == 10); 41 | ``` 42 | 43 | ```js 44 | // This is context code available everywhere 45 | // The user will be able to call magicFunc in his code 46 | function magicFunc() { 47 | return 3; 48 | } 49 | ``` 50 | 51 | --- 52 | 53 | ## Another exercise 54 | 55 | Bla bla bla ... This time with no `context` code. 56 | 57 | 58 | ```js 59 | var x = 60 | ``` 61 | 62 | ```js 63 | var x = 10; 64 | ``` 65 | 66 | ```js 67 | assert(x == 10); 68 | ``` 69 | -------------------------------------------------------------------------------- /test/fixtures/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Chapter 1](chapter-1/README.md) 4 | * [Article 1](chapter-1/ARTICLE1.md) 5 | * [Article 2](chapter-1/ARTICLE2.md) 6 | * [article 1.2.1](\chapter-1\ARTICLE-1-2-1.md) 7 | * [article 1.2.2](/chapter-1/ARTICLE-1-2-2.md) 8 | * [Chapter 2](chapter-2/README.md) 9 | * [Chapter 3](chapter-3/README.md) 10 | * [Chapter 4](chapter-4/README.md) 11 | * Unfinished article 12 | * Unfinished Chapter 13 | -------------------------------------------------------------------------------- /test/fixtures/SUMMARY_WHITESPACE.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Chapter 1](chapter-1/README.md) 4 | * [Article 1](chapter-1/ARTICLE1.md) 5 | * [Article 2](chapter-1/ARTICLE2.md) 6 | * [article 1.2.1](\chapter-1\ARTICLE-1-2-1.md) 7 | * [article 1.2.2](/chapter-1/ARTICLE-1-2-2.md) 8 | 9 | * [Chapter 2](chapter-2/README.md) 10 | * [Chapter 3](chapter-3/README.md) 11 | * [Chapter 4](chapter-4/README.md) 12 | 13 | * Unfinished article 14 | 15 | * Unfinished Chapter 16 | -------------------------------------------------------------------------------- /test/fixtures/book1/README.md: -------------------------------------------------------------------------------- 1 | # This is a test 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/book1/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [test](test.md) -------------------------------------------------------------------------------- /test/fixtures/book1/test.md: -------------------------------------------------------------------------------- 1 | # Test 2 | -------------------------------------------------------------------------------- /test/fixtures/book2/README.md: -------------------------------------------------------------------------------- 1 | # This should fail 2 | -------------------------------------------------------------------------------- /test/fixtures/included.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[]) { 4 | printf("All is well\n"); 5 | 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /test/generate.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Q = require('q'); 3 | var path = require('path'); 4 | var tmp = require('tmp'); 5 | var assert = require('assert'); 6 | 7 | var generate = require("../lib/generate"); 8 | 9 | var generateTmpBook = function(path) { 10 | return ; 11 | }; 12 | 13 | 14 | var BOOKS = { 15 | "book1": true, 16 | "book2": false 17 | }; 18 | 19 | describe('Site Generation', function () { 20 | var ret = {}; 21 | 22 | beforeEach(function(done){ 23 | Q.all(_.map(BOOKS, function(state, bookName) { 24 | return Q.nfcall(tmp.dir) 25 | .then(function(_dir) { 26 | return generate.folder({ 27 | input: path.join(__dirname, "fixtures", bookName), 28 | output: _dir 29 | }); 30 | }) 31 | .then(function(_book) { 32 | ret[bookName] = _book; 33 | }, function(err) { 34 | // ignore errors here 35 | }); 36 | })) 37 | .fin(done); 38 | }); 39 | 40 | 41 | it('should generate the valid sites', function() { 42 | _.each(BOOKS, function(state, bookName) { 43 | assert((ret[bookName] != null) == state); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/glossary.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var glossary = require('../').parse.glossary; 6 | 7 | var CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/GLOSSARY.md'), 'utf8'); 8 | var LEXED = glossary(CONTENT); 9 | 10 | describe('Glossary parsing', function () { 11 | it('should only get heading + paragraph pairs', function() { 12 | assert.equal(LEXED.length, 5); 13 | }); 14 | 15 | it('should output simple name/description objects', function() { 16 | assert.equal(true, !(LEXED.some(function(e) { 17 | return !Boolean(e.name && e.description); 18 | }))); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/includes.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var page = require('../').parse.page; 6 | var includer = require('../').parse.includer; 7 | 8 | var FIXTURES_DIR = path.join(__dirname, './fixtures/'); 9 | 10 | function loadPage (name, options) { 11 | var CONTENT = fs.readFileSync(FIXTURES_DIR + name + '.md', 'utf8'); 12 | return page(CONTENT, options).sections; 13 | } 14 | 15 | 16 | describe('Code includes', function() { 17 | 18 | var LEXED = loadPage('INCLUDES', { 19 | 'dir': FIXTURES_DIR, 20 | 'includer': includer({}, [ 21 | FIXTURES_DIR 22 | ], path.join, fs.readFileSync) 23 | }); 24 | 25 | var INCLUDED_C = fs.readFileSync(path.join(FIXTURES_DIR, 'included.c'), 'utf8'); 26 | 27 | it('should work for snippets', function() { 28 | assert.equal(LEXED[0].type, 'normal'); 29 | // Has replaced include 30 | assert.equal( 31 | LEXED[0].content.indexOf('{{ included.c }}'), 32 | -1 33 | ); 34 | }); 35 | 36 | it('should work for exercises', function() { 37 | assert.equal(LEXED[1].type, 'exercise'); 38 | 39 | // Solution is trimmed version of source 40 | assert.equal(LEXED[1].code.solution, INCLUDED_C.trim()); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/navigation.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var summary = require('../').parse.summary; 6 | var navigation = require('../').parse.navigation; 7 | 8 | 9 | var CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/SUMMARY.md'), 'utf8'); 10 | var ALT_CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/ALTERNATIVE_SUMMARY.md'), 'utf8'); 11 | var LEXED = summary(CONTENT); 12 | var ALT_LEXED = summary(ALT_CONTENT); 13 | 14 | 15 | describe('Summary navigation', function() { 16 | it('should provide next & prev entries for a file', function() { 17 | var nav = navigation(LEXED, [ 18 | 'README.md', 19 | 'chapter-1/README.md', 20 | 'chapter-1/ARTICLE1.md', 21 | 'chapter-1/ARTICLE2.md', 22 | 'chapter-2/README.md', 23 | 'chapter-1/ARTICLE-1-2-1.md', 24 | 'chapter-1/ARTICLE-1-2-2.md' 25 | ]); 26 | 27 | // Make sure it found the files we gave it 28 | assert(nav['README.md']); 29 | assert(nav['chapter-1/README.md']); 30 | assert(nav['chapter-1/ARTICLE1.md']); 31 | assert(nav['chapter-1/ARTICLE2.md']); 32 | assert(nav['chapter-2/README.md']); 33 | assert(nav['chapter-1/ARTICLE-1-2-1.md']); 34 | assert(nav['chapter-1/ARTICLE-1-2-2.md']); 35 | 36 | 37 | assert.equal(nav['README.md'].prev, null); 38 | assert.equal(nav['README.md'].next.path, 'chapter-1/README.md'); 39 | 40 | assert.equal(nav['chapter-1/README.md'].prev.path, 'README.md'); 41 | assert.equal(nav['chapter-1/README.md'].next.path, 'chapter-1/ARTICLE1.md'); 42 | 43 | assert.equal(nav['chapter-1/ARTICLE1.md'].prev.path, 'chapter-1/README.md'); 44 | assert.equal(nav['chapter-1/ARTICLE1.md'].next.path, 'chapter-1/ARTICLE2.md'); 45 | 46 | assert.equal(nav['chapter-1/ARTICLE2.md'].prev.path, 'chapter-1/ARTICLE1.md'); 47 | assert.equal(nav['chapter-1/ARTICLE2.md'].next.path, 'chapter-1/ARTICLE-1-2-1.md'); 48 | 49 | assert.equal(nav['chapter-1/ARTICLE-1-2-1.md'].prev.path, 'chapter-1/ARTICLE2.md'); 50 | assert.equal(nav['chapter-1/ARTICLE-1-2-1.md'].next.path, 'chapter-1/ARTICLE-1-2-2.md'); 51 | 52 | assert.equal(nav['chapter-1/ARTICLE-1-2-2.md'].prev.path, 'chapter-1/ARTICLE-1-2-1.md'); 53 | assert.equal(nav['chapter-1/ARTICLE-1-2-2.md'].next.path, 'chapter-2/README.md'); 54 | 55 | assert.equal(nav['chapter-2/README.md'].prev.path, 'chapter-1/ARTICLE-1-2-2.md'); 56 | assert.equal(nav['chapter-2/README.md'].next.path, 'chapter-3/README.md'); 57 | }); 58 | 59 | it('should give full tree, when not limited', function() { 60 | var nav = navigation(LEXED); 61 | 62 | assert(nav['README.md']); 63 | assert(nav['chapter-1/README.md']); 64 | assert(nav['chapter-1/ARTICLE1.md']); 65 | assert(nav['chapter-1/ARTICLE2.md']); 66 | assert(nav['chapter-2/README.md']); 67 | assert(nav['chapter-3/README.md']); 68 | }); 69 | 70 | it('should detect levels correctly', function() { 71 | var nav = navigation(LEXED); 72 | 73 | assert.equal(nav['README.md'].level, '0'); 74 | assert.equal(nav['chapter-1/README.md'].level, '1'); 75 | assert.equal(nav['chapter-1/ARTICLE1.md'].level, '1.1'); 76 | assert.equal(nav['chapter-1/ARTICLE2.md'].level, '1.2'); 77 | assert.equal(nav['chapter-2/README.md'].level, '2'); 78 | assert.equal(nav['chapter-3/README.md'].level, '3'); 79 | }); 80 | 81 | it('should have a default README node', function() { 82 | var nav = navigation(LEXED); 83 | 84 | assert.equal(nav['README.md'].level, '0'); 85 | assert.equal(nav['README.md'].title, 'Introduction'); 86 | }); 87 | 88 | it('Should allow README node to be customized', function() { 89 | var nav = navigation(ALT_LEXED); 90 | 91 | assert(nav['README.md']); 92 | assert.equal(nav['README.md'].level, '0'); 93 | assert.notEqual(nav['README.md'].title, 'Introduction'); 94 | }); 95 | 96 | it('should not accept null paths', function() { 97 | var nav = navigation(LEXED); 98 | 99 | assert(!nav[null]); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/page.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var page = require('../').parse.page; 6 | 7 | function loadPage (name, options) { 8 | var CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/' + name + '.md'), 'utf8'); 9 | return page(CONTENT, options).sections; 10 | } 11 | 12 | var LEXED = loadPage('PAGE', { 13 | dir: 'course', 14 | outdir: '_book' 15 | }); 16 | var QUIZ_LEXED = loadPage('QUIZ_PAGE'); 17 | var HR_LEXED = loadPage('HR_PAGE'); 18 | 19 | describe('Page parsing', function() { 20 | it('should detect sections', function() { 21 | assert.equal(LEXED.length, 4); 22 | }); 23 | 24 | it('should detect section types', function() { 25 | assert.equal(LEXED[0].type, 'normal'); 26 | assert.equal(LEXED[1].type, 'exercise'); 27 | assert.equal(LEXED[2].type, 'normal'); 28 | assert.equal(QUIZ_LEXED[0].type, 'normal'); 29 | assert.equal(QUIZ_LEXED[1].type, 'quiz'); 30 | assert.equal(QUIZ_LEXED[2].type, 'normal'); 31 | assert.equal(QUIZ_LEXED[3].type, 'quiz'); 32 | }); 33 | 34 | it('should gen content for normal sections', function() { 35 | assert(LEXED[0].content); 36 | assert(LEXED[2].content); 37 | }); 38 | 39 | it('should gen code and content for exercise sections', function() { 40 | assert(LEXED[1].content); 41 | assert(LEXED[1].code); 42 | assert(LEXED[1].code.base); 43 | assert(LEXED[1].code.solution); 44 | assert(LEXED[1].code.validation); 45 | assert(LEXED[1].code.context === null); 46 | 47 | assert(LEXED[3].content); 48 | assert(LEXED[3].code); 49 | assert(LEXED[3].code.base); 50 | assert(LEXED[3].code.solution); 51 | assert(LEXED[3].code.validation); 52 | assert(LEXED[3].code.context); 53 | }); 54 | 55 | it('should merge sections correctly', function() { 56 | // One big section 57 | assert.equal(HR_LEXED.length, 1); 58 | 59 | // HRs inserted correctly 60 | assert.equal(HR_LEXED[0].content.match(/
/g).length, 2); 61 | }); 62 | 63 | it('should detect an exercise\'s language', function() { 64 | assert.equal(LEXED[1].lang, 'python'); 65 | }); 66 | 67 | it('should render a quiz', function() { 68 | assert(QUIZ_LEXED[1].content); 69 | assert(QUIZ_LEXED[1].quiz); 70 | assert(QUIZ_LEXED[1].quiz[0].base); 71 | assert(QUIZ_LEXED[1].quiz[0].solution); 72 | assert(QUIZ_LEXED[1].quiz[0].feedback); 73 | assert(QUIZ_LEXED[1].quiz[1].base); 74 | assert(QUIZ_LEXED[1].quiz[1].solution); 75 | assert(QUIZ_LEXED[1].quiz[1].feedback); 76 | }); 77 | }); 78 | 79 | 80 | describe('Relative links', function() { 81 | it('should replace link to .md by link to .html', function() { 82 | var LEXED = loadPage('MARKDOWN_LINKS', { 83 | // Imaginary folder of markdown file 84 | dir: 'course', 85 | outdir: 'course' 86 | }); 87 | 88 | assert(LEXED[0].content.indexOf('test.html') !== -1); 89 | assert(LEXED[0].content.indexOf('../before.html') !== -1); 90 | }); 91 | }); 92 | 93 | describe('Relative images', function() { 94 | it('should keep image relative with considering output directory in site format', function() { 95 | var LEXED = loadPage('IMAGES', { 96 | // Imaginary folder of markdown file 97 | dir: 'syntax', 98 | outdir: 'syntax' 99 | }); 100 | 101 | assert(LEXED[0].content.indexOf('"preview.png"') !== -1); 102 | assert(LEXED[0].content.indexOf('"../preview2.png"') !== -1); 103 | }); 104 | 105 | it('should keep image relative with considering output directory in page format', function() { 106 | var LEXED = loadPage('IMAGES', { 107 | // Imaginary folder of markdown file 108 | dir: 'syntax', 109 | outdir: './' 110 | }); 111 | 112 | assert(LEXED[0].content.indexOf('"syntax/preview.png"') !== -1); 113 | assert(LEXED[0].content.indexOf('"preview2.png"') !== -1); 114 | }); 115 | }); 116 | 117 | describe('Section parsing', function() { 118 | it('should not have false positive quiz parsing', function() { 119 | var LEXED = loadPage('FALSE_QUIZ'); 120 | 121 | assert.equal(LEXED[0].type, 'normal'); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var Plugin = require('../').generate.Plugin; 6 | 7 | describe('Plugin validation', function () { 8 | var plugin = new Plugin("plugin", __dirname); 9 | 10 | it('should be valid', function() { 11 | assert(plugin.isValid()); 12 | }); 13 | }); 14 | 15 | describe('Plugins list', function () { 16 | var firstDefault = _.first(Plugin.defaults); 17 | 18 | it('should convert string to array', function() { 19 | var _name = "test"; 20 | assert(_.contains(Plugin.normalizeNames(_name), _name)); 21 | }); 22 | 23 | it('should contains default plugins', function() { 24 | assert(_.contains(Plugin.normalizeNames([]), firstDefault)); 25 | }); 26 | 27 | it('should remove name starting with -', function() { 28 | assert(!_.contains(Plugin.normalizeNames(["-"+firstDefault]), firstDefault)); 29 | }); 30 | 31 | it('should accept version', function() { 32 | var _name = "test@0.3.0,exercises@1.2.0,test2"; 33 | var plugins = Plugin.normalizeList(_name); 34 | 35 | assert(_.find(plugins, {'name': "test"}).version = "0.3.0"); 36 | assert(_.find(plugins, {'name': "exercises"}).version = "1.2.0"); 37 | assert(!_.find(plugins, {'name': "test2"}).version); 38 | }); 39 | }); 40 | 41 | describe('Plugin defaults loading', function () { 42 | var ret = true; 43 | 44 | beforeEach(function(done){ 45 | Plugin.fromList(Plugin.defaults, __dirname) 46 | .then(function(_r) { 47 | ret = _r; 48 | }, function(err) { 49 | ret = null; 50 | }) 51 | .fin(done); 52 | }); 53 | 54 | 55 | it('should load defaults addons', function() { 56 | assert(ret != null); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/readme.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var readme = require('../').parse.readme; 6 | 7 | 8 | var CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/README.md'), 'utf8'); 9 | var LEXED = readme(CONTENT); 10 | 11 | describe('Readme parsing', function () { 12 | 13 | it('should contain a title', function() { 14 | assert(LEXED.title); 15 | }); 16 | 17 | it('should contain a description', function() { 18 | assert(LEXED.description); 19 | }); 20 | 21 | it('should extract the right title', function() { 22 | assert.equal(LEXED.title, "This is the title"); 23 | }); 24 | 25 | it('should extract the right description', function() { 26 | assert.equal(LEXED.description, "This is the book description."); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/sections.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var lex = require('../').parse.lex; 6 | 7 | 8 | var CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/SECTIONS.md'), 'utf8'); 9 | var LEXED = lex(CONTENT); 10 | 11 | 12 | describe('Section parsing', function() { 13 | it('should correctly split sections', function() { 14 | assert.equal(LEXED.length, 3); 15 | }); 16 | 17 | it('should robustly detect exercises', function() { 18 | assert.equal(LEXED[0].type, 'normal'); 19 | assert.equal(LEXED[1].type, 'exercise'); 20 | assert.equal(LEXED[2].type, 'exercise'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/summary.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | 5 | var summary = require('../').parse.summary; 6 | 7 | function lex(fixtureFile) { 8 | return summary( 9 | fs.readFileSync( 10 | path.join(__dirname, 'fixtures', fixtureFile), 11 | 'utf8' 12 | ) 13 | ); 14 | } 15 | 16 | var LEXED = lex('SUMMARY.md'); 17 | 18 | describe('Summary parsing', function () { 19 | 20 | it('should detect chapters', function() { 21 | assert.equal(LEXED.chapters.length, 6); 22 | }); 23 | 24 | it('should support articles', function() { 25 | assert.equal(LEXED.chapters[1].articles.length, 2); 26 | assert.equal(LEXED.chapters[2].articles.length, 0); 27 | assert.equal(LEXED.chapters[3].articles.length, 0); 28 | }); 29 | 30 | it('should detect paths and titles', function() { 31 | assert(LEXED.chapters[0].path); 32 | assert(LEXED.chapters[1].path); 33 | assert(LEXED.chapters[2].path); 34 | assert(LEXED.chapters[3].path); 35 | assert(LEXED.chapters[4].path); 36 | assert.equal(LEXED.chapters[5].path, null); 37 | 38 | assert(LEXED.chapters[0].title); 39 | assert(LEXED.chapters[1].title); 40 | assert(LEXED.chapters[2].title); 41 | assert(LEXED.chapters[3].title); 42 | assert(LEXED.chapters[4].title); 43 | assert(LEXED.chapters[5].title); 44 | }); 45 | 46 | it('should normalize paths from .md to .html', function() { 47 | assert.equal(LEXED.chapters[0].path,'README.md'); 48 | assert.equal(LEXED.chapters[1].path,'chapter-1/README.md'); 49 | assert.equal(LEXED.chapters[2].path,'chapter-2/README.md'); 50 | assert.equal(LEXED.chapters[3].path,'chapter-3/README.md'); 51 | }); 52 | 53 | it('should detect levels correctly', function() { 54 | var c = LEXED.chapters; 55 | 56 | assert.equal(c[0].level, '0'); 57 | assert.equal(c[1].level, '1'); 58 | assert.equal(c[2].level, '2'); 59 | assert.equal(c[3].level, '3'); 60 | 61 | assert.equal(c[1].articles[0].level, '1.1'); 62 | assert.equal(c[1].articles[1].level, '1.2'); 63 | assert.equal(c[1].articles[1].articles[0].level, '1.2.1'); 64 | }); 65 | 66 | it('should allow lists separated by whitespace', function() { 67 | var l = lex('SUMMARY_WHITESPACE.md'); 68 | 69 | assert.equal(l.chapters.length, 6); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /theme/assets/fonts/fontawesome/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/fontawesome/FontAwesome.otf -------------------------------------------------------------------------------- /theme/assets/fonts/fontawesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/fontawesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /theme/assets/fonts/fontawesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/fontawesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /theme/assets/fonts/fontawesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/fontawesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/250.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/250.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/250i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/250i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/400.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/400i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/400i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/700.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/700i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/700i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/900.woff -------------------------------------------------------------------------------- /theme/assets/fonts/merriweather/900i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/merriweather/900i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/300.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/300i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/300i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/400.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/400i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/400i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/600.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/600i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/600i.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/700.woff -------------------------------------------------------------------------------- /theme/assets/fonts/opensans/700i.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/fonts/opensans/700i.woff -------------------------------------------------------------------------------- /theme/assets/images/apple-touch-icon-precomposed-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/images/apple-touch-icon-precomposed-152.png -------------------------------------------------------------------------------- /theme/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codepiano/gitbook/33fbc24a20599c83b18b3d8b4607cdbace833d17/theme/assets/images/favicon.ico -------------------------------------------------------------------------------- /theme/assets/print.css: -------------------------------------------------------------------------------- 1 | .link-inherit{color:inherit}.link-inherit:hover,.link-inherit:focus{color:inherit}.hidden{display:none}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}.hljs-comment,.hljs-title{color:#8e908c}.hljs-variable,.hljs-attribute,.hljs-tag,.hljs-regexp,.ruby .hljs-constant,.xml .hljs-tag .hljs-title,.xml .hljs-pi,.xml .hljs-doctype,.html .hljs-doctype,.css .hljs-id,.css .hljs-class,.css .hljs-pseudo{color:#c82829}.hljs-number,.hljs-preprocessor,.hljs-pragma,.hljs-built_in,.hljs-literal,.hljs-params,.hljs-constant{color:#f5871f}.ruby .hljs-class .hljs-title,.css .hljs-rules .hljs-attribute{color:#eab700}.hljs-string,.hljs-value,.hljs-inheritance,.hljs-header,.ruby .hljs-symbol,.xml .hljs-cdata{color:#718c00}.css .hljs-hexcolor{color:#3e999f}.hljs-function,.python .hljs-decorator,.python .hljs-title,.ruby .hljs-function .hljs-title,.ruby .hljs-title .hljs-keyword,.perl .hljs-sub,.javascript .hljs-title,.coffeescript .hljs-title{color:#4271ae}.hljs-keyword,.javascript .hljs-function{color:#8959a8}.hljs{display:block;background:white;color:#4d4d4c;padding:.5em}.coffeescript .javascript,.javascript .xml,.tex .hljs-formula,.xml .javascript,.xml .vbscript,.xml .css,.xml .hljs-cdata{opacity:.5}.page.page-toc ol{margin:0}.page .book-chapter{display:none}.page .exercise,.page .quiz{margin:1cm 0;border:2px solid #ddd}.page .exercise .exercise-header,.page .quiz .exercise-header{padding:.1cm .3cm;background:#f5f5f5;border-bottom:1px solid #ddd;font-weight:bold;page-break-inside:avoid}.page .exercise .exercise-inner,.page .quiz .exercise-inner{padding:.15cm .3cm}.page .exercise .exercise-inner p:last-child,.page .quiz .exercise-inner p:last-child{margin:0}.page .exercise hr,.page .quiz hr{height:2px}.page .exercise .question,.page .quiz .question{margin-top:.1cm;border-top:1px solid #ddd}body .page{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.4;color:#333;overflow:hidden;line-height:1.6;word-wrap:break-word;display:block}body .page>*:first-child{margin-top:0!important}body .page>*:last-child{margin-bottom:0!important}body .page a{background:transparent}body .page a:active,body .page a:hover{outline:0}body .page strong{font-weight:bold}body .page h1{font-size:2em;margin:.67em 0}body .page img{border:0}body .page hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}body .page pre{overflow:auto}body .page code,body .page pre{font-family:monospace,monospace;font-size:1em}body .page table{border-collapse:collapse;border-spacing:0}body .page td,body .page th{padding:0}body .page *{-moz-box-sizing:border-box;box-sizing:border-box}body .page a{color:#4183c4;text-decoration:none}body .page a:hover,body .page a:focus,body .page a:active{text-decoration:underline}body .page hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}body .page hr:before,body .page hr:after{display:table;content:" "}body .page hr:after{clear:both}body .page h1,body .page h2,body .page h3,body .page h4,body .page h5,body .page h6{margin-top:15px;margin-bottom:15px;line-height:1.1}body .page h1{font-size:30px}body .page h2{font-size:21px}body .page h3{font-size:16px}body .page h4{font-size:14px}body .page h5{font-size:12px}body .page h6{font-size:11px}body .page blockquote{margin:0}body .page ul,body .page ol{padding:0;margin-top:0;margin-bottom:0}body .page ol ol{list-style-type:lower-roman}body .page dd{margin-left:0}body .page code,body .page pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px}body .page pre{margin-top:0;margin-bottom:0}body .page .markdown-body>*:first-child{margin-top:0!important}body .page .markdown-body>*:last-child{margin-bottom:0!important}body .page .anchor{position:absolute;top:0;bottom:0;left:0;display:block;padding-right:6px;padding-left:30px;margin-left:-30px}body .page .anchor:focus{outline:0}body .page h1,body .page h2,body .page h3,body .page h4,body .page h5,body .page h6{position:relative;margin-top:1em;margin-bottom:16px;font-weight:bold;line-height:1.4}body .page h1{padding-bottom:.3em;font-size:2.25em;line-height:1.2;border-bottom:1px solid #eee}body .page h2{padding-bottom:.3em;font-size:1.75em;line-height:1.225;border-bottom:1px solid #eee}body .page h3{font-size:1.5em;line-height:1.43}body .page h4{font-size:1.25em}body .page h5{font-size:1em}body .page h6{font-size:1em;color:#777}body .page p,body .page blockquote,body .page ul,body .page ol,body .page dl,body .page table,body .page pre{margin-top:0;margin-bottom:16px}body .page hr{height:4px;padding:0;margin:16px 0;background-color:#e7e7e7;border:0 none}body .page ul,body .page ol{padding-left:2em}body .page ol ol,body .page ol ul,body .page ul ul{margin-top:0;margin-bottom:0}body .page dl{padding:0}body .page dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:bold}body .page dl dd{padding:0 16px;margin-bottom:16px}body .page blockquote{padding:0 15px;color:#777;border-left:4px solid #ddd}body .page blockquote>:first-child{margin-top:0}body .page blockquote>:last-child{margin-bottom:0}body .page table{display:block;width:100%;overflow:auto}body .page table th{font-weight:bold}body .page table th,body .page table td{padding:6px 13px;border:1px solid #ddd}body .page table tr{background-color:#fff;border-top:1px solid #ccc}body .page table tr:nth-child(2n){background-color:#f8f8f8}body .page img{max-width:100%;-moz-box-sizing:border-box;box-sizing:border-box;page-break-inside:avoid}body .page code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:#f7f7f7;border-radius:3px}body .page code:before,body .page code:after{letter-spacing:-0.2em;content:"\00a0"}body .page pre>code{padding:0;margin:0;font-size:100%;white-space:pre;background:transparent;border:0}body .page .highlight pre,body .page pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f7;border:0;border-radius:3px}body .page pre{word-wrap:normal}body .page pre code{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}body .page pre code:before,body .page pre code:after{content:normal}body .page .highlight{background:#fff} -------------------------------------------------------------------------------- /theme/javascript/core/events.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery" 3 | ], function($) { 4 | var events = $({}); 5 | 6 | return events; 7 | }); -------------------------------------------------------------------------------- /theme/javascript/core/font-settings.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "utils/storage" 4 | ], function($, storage) { 5 | var fontState; 6 | 7 | var THEMES = { 8 | "white": 0, 9 | "sepia": 1, 10 | "night": 2 11 | }; 12 | 13 | var FAMILY = { 14 | "serif": 0, 15 | "sans": 1 16 | }; 17 | 18 | var enlargeFontSize = function(e){ 19 | if (fontState.size < 4){ 20 | fontState.size++; 21 | fontState.save(); 22 | } 23 | }; 24 | 25 | var reduceFontSize = function(e){ 26 | if (fontState.size > 0){ 27 | fontState.size--; 28 | fontState.save(); 29 | } 30 | }; 31 | 32 | var changeFontFamily = function(){ 33 | var index = $(this).data("font"); 34 | 35 | fontState.family = index; 36 | fontState.save(); 37 | }; 38 | 39 | var changeColorTheme = function(){ 40 | var $book = $(".book"); 41 | var index = $(this).data("theme"); 42 | 43 | if (fontState.theme !== 0) 44 | $book.removeClass("color-theme-"+fontState.theme); 45 | 46 | fontState.theme = index; 47 | if (fontState.theme !== 0) 48 | $book.addClass("color-theme-"+fontState.theme); 49 | 50 | fontState.save(); 51 | }; 52 | 53 | var update = function() { 54 | var $book = $(".book"); 55 | 56 | $(".font-settings .font-family-list li").removeClass("active"); 57 | $(".font-settings .font-family-list li:nth-child("+(fontState.family+1)+")").addClass("active"); 58 | 59 | $book[0].className = $book[0].className.replace(/\bfont-\S+/g, ''); 60 | $book.addClass("font-size-"+fontState.size); 61 | $book.addClass("font-family-"+fontState.family); 62 | 63 | if(fontState.theme !== 0) { 64 | $book[0].className = $book[0].className.replace(/\bcolor-theme-\S+/g, ''); 65 | $book.addClass("color-theme-"+fontState.theme); 66 | } 67 | }; 68 | 69 | var init = function(config) { 70 | var $toggle, $bookBody, $dropdown, $book; 71 | 72 | //Find DOM elements. 73 | $book = $(".book"); 74 | $toggle = $(".book-header .toggle-font-settings"); 75 | $dropdown = $("#font-settings-wrapper .dropdown-menu"); 76 | $bookBody = $(".book-body"); 77 | 78 | // Instantiate font state object 79 | fontState = storage.get("fontState", { 80 | size: config.size || 2, 81 | family: FAMILY[config.family || "sans"], 82 | theme: THEMES[config.theme || "white"] 83 | }); 84 | fontState.save = function(){ 85 | storage.set("fontState",fontState); 86 | update(); 87 | }; 88 | 89 | update(); 90 | 91 | //Add event listeners 92 | $(document).on('click', "#enlarge-font-size", enlargeFontSize); 93 | $(document).on('click', "#reduce-font-size", reduceFontSize); 94 | 95 | $(document).on('click', "#font-settings-wrapper .font-family-list .button", changeFontFamily); 96 | $(document).on('click', "#font-settings-wrapper .color-theme-list .button", changeColorTheme); 97 | }; 98 | 99 | return { 100 | init: init, 101 | update: update 102 | } 103 | }); -------------------------------------------------------------------------------- /theme/javascript/core/glossary.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "lodash", 4 | "core/state" 5 | ], function($, _, state) { 6 | var index = null; 7 | 8 | // Use a specific idnex 9 | var useIndex = function(data) { 10 | index = data; 11 | }; 12 | 13 | // Load complete index 14 | var loadIndex = function() { 15 | return $.getJSON(state.basePath+"/glossary_index.json") 16 | .then(useIndex); 17 | }; 18 | 19 | // Get index and return a promise 20 | var getIndex = function() { 21 | var d = $.Deferred(); 22 | 23 | if (index) { 24 | d.resolve(index); 25 | } else { 26 | loadIndex().done(function(){ 27 | d.resolve(index); 28 | }).fail(d.reject); 29 | } 30 | 31 | return d.promise(); 32 | } 33 | 34 | $.fn.replaceText = function( search, replace, text_only ) { 35 | return this.each(function(){ 36 | var node = this.firstChild, 37 | val, 38 | new_val, 39 | 40 | // Elements to be removed at the end. 41 | remove = []; 42 | 43 | // Only continue if firstChild exists. 44 | if ( node ) { 45 | 46 | // Loop over all childNodes. 47 | do { 48 | 49 | // Only process text nodes. 50 | if ( node.nodeType === 3 ) { 51 | 52 | // The original node value. 53 | val = node.nodeValue; 54 | 55 | // The new value. 56 | new_val = val.replace( search, replace ); 57 | 58 | // Only replace text if the new value is actually different! 59 | if ( new_val !== val ) { 60 | 61 | if ( !text_only && /\|\:])/g, "\\$1"); 86 | }; 87 | 88 | var init = function() { 89 | // Bind click on glossary item 90 | $(document).on("click", ".book-body .page-wrapper .page-inner .glossary-term", function(e) { 91 | e.preventDefault(); 92 | 93 | location.href = state.basePath+"/GLOSSARY.html#"+$(e.currentTarget).data("glossary-term"); 94 | }); 95 | }; 96 | 97 | var replaceTerm = function($el, term) { 98 | var r = new RegExp( "\\b(" + pregQuote(term.name.toLowerCase()) + ")\\b" , 'gi' ); 99 | 100 | $el.find("*").replaceText(r, function(match) { 101 | return ""+match+""; 102 | }); 103 | 104 | }; 105 | 106 | var prepare = function() { 107 | getIndex() 108 | .done(function() { 109 | _.each(index, _.partial(replaceTerm, $(".book-body .page-wrapper .page-inner"))); 110 | }); 111 | }; 112 | 113 | return { 114 | init: init, 115 | prepare: prepare 116 | }; 117 | }); -------------------------------------------------------------------------------- /theme/javascript/core/keyboard.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "Mousetrap", 4 | "core/navigation", 5 | "core/sidebar", 6 | "core/search" 7 | ], function($, Mousetrap, navigation, sidebar, search){ 8 | // Bind keyboard shortcuts 9 | var init = function() { 10 | // Next 11 | Mousetrap.bind(['right'], function(e) { 12 | navigation.goNext(); 13 | return false; 14 | }); 15 | 16 | // Prev 17 | Mousetrap.bind(['left'], function(e) { 18 | navigation.goPrev(); 19 | return false; 20 | }); 21 | 22 | // Toggle Summary 23 | Mousetrap.bind(['s'], function(e) { 24 | sidebar.toggle(); 25 | return false; 26 | }); 27 | 28 | // Toggle Search 29 | Mousetrap.bind(['f'], function(e) { 30 | search.toggle(); 31 | return false; 32 | }); 33 | }; 34 | 35 | return { 36 | init: init 37 | }; 38 | }); -------------------------------------------------------------------------------- /theme/javascript/core/loading.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery" 3 | ], function($) { 4 | var showLoading = function(p) { 5 | $(".book").addClass("is-loading"); 6 | p.always(function() { 7 | $(".book").removeClass("is-loading"); 8 | }); 9 | 10 | return p; 11 | }; 12 | 13 | return { 14 | show: showLoading 15 | }; 16 | }); -------------------------------------------------------------------------------- /theme/javascript/core/navigation.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "utils/url", 4 | "core/events", 5 | "core/state", 6 | "core/progress", 7 | "core/loading", 8 | "core/search", 9 | "core/glossary" 10 | ], function($, URL, events, state, progress, loading, search, glossary) { 11 | var prev, next; 12 | 13 | var usePushState = (typeof history.pushState !== "undefined"); 14 | 15 | var handleNavigation = function(relativeUrl, push) { 16 | var url = URL.join(window.location.pathname, relativeUrl); 17 | console.log("navigate to ", url, "baseurl="+relativeUrl, "current="+window.location.pathname); 18 | 19 | if (!usePushState) { 20 | // Refresh the page to the new URL if pushState not supported 21 | location.href = relativeUrl; 22 | return 23 | } 24 | 25 | return loading.show($.get(url) 26 | .done(function (html) { 27 | // Push url to history 28 | if (push) history.pushState({ path: url }, null, url); 29 | 30 | // Replace html content 31 | html = html.replace( /<(\/?)(html|head|body)([^>]*)>/ig, function(a,b,c,d){ 32 | return '<' + b + 'div' + ( b ? '' : ' data-element="' + c + '"' ) + d + '>'; 33 | }); 34 | 35 | var $page = $(html); 36 | var $pageHead = $page.find("[data-element=head]"); 37 | var $pageBody = $page.find('.book'); 38 | 39 | // Merge heads 40 | var headContent = $pageHead.html() 41 | 42 | $("head style").each(function() { 43 | headContent = headContent + this.outerHTML 44 | }); 45 | $("head").html(headContent); 46 | 47 | // Merge body 48 | var bodyClass = $(".book").attr("class"); 49 | var scrollPosition = $('.book-summary .summary').scrollTop(); 50 | $pageBody.toggleClass("with-summary", $(".book").hasClass("with-summary")) 51 | 52 | $(".book").replaceWith($pageBody); 53 | $(".book").attr("class", bodyClass); 54 | $('.book-summary .summary').scrollTop(scrollPosition); 55 | 56 | // Update state 57 | state.update($("html")); 58 | // recover search keyword 59 | search.recover(); 60 | preparePage(); 61 | }) 62 | .fail(function (e) { 63 | location.href = relativeUrl; 64 | })); 65 | }; 66 | 67 | var updateNavigationPosition = function() { 68 | var bodyInnerWidth, pageWrapperWidth; 69 | 70 | bodyInnerWidth = parseInt($('.body-inner').css('width'), 10); 71 | pageWrapperWidth = parseInt($('.page-wrapper').css('width'), 10); 72 | $('.navigation-next').css('margin-right', (bodyInnerWidth - pageWrapperWidth) + 'px'); 73 | }; 74 | 75 | var preparePage = function() { 76 | var $pageWrapper = $(".book-body .page-wrapper"); 77 | 78 | // Show progress 79 | progress.show(); 80 | 81 | // Update navigation position 82 | updateNavigationPosition(); 83 | 84 | // Set glossary items 85 | glossary.prepare(); 86 | 87 | // Reset scroll 88 | $pageWrapper.scrollTop(0); 89 | 90 | // Focus on content 91 | $pageWrapper.focus(); 92 | 93 | // Notify 94 | events.trigger("page.change"); 95 | }; 96 | 97 | var handlePagination = function (e) { 98 | e.stopPropagation(); 99 | e.preventDefault(); 100 | 101 | var url = $(this).attr('href'); 102 | if (url) handleNavigation(url, true); 103 | }; 104 | 105 | var goNext = function() { 106 | var url = $(".navigation-next").attr("href"); 107 | if (url) handleNavigation(url, true); 108 | }; 109 | 110 | var goPrev = function() { 111 | var url = $(".navigation-prev").attr("href"); 112 | if (url) handleNavigation(url, true); 113 | }; 114 | 115 | 116 | 117 | var init = function() { 118 | // Prevent cache so that using the back button works 119 | // See: http://stackoverflow.com/a/15805399/983070 120 | $.ajaxSetup({ 121 | cache: false 122 | }); 123 | 124 | // Recreate first page when the page loads. 125 | history.replaceState({ path: window.location.href }, ''); 126 | 127 | // Back Button Hijacking :( 128 | window.onpopstate = function (event) { 129 | if (event.state === null) { 130 | return; 131 | } 132 | return handleNavigation(event.state.path, false); 133 | }; 134 | 135 | $(document).on('click', ".navigation-prev", handlePagination); 136 | $(document).on('click', ".navigation-next", handlePagination); 137 | $(document).on('click', ".summary [data-path] a", handlePagination); 138 | 139 | $(window).resize(updateNavigationPosition); 140 | 141 | // Prepare current page 142 | preparePage(); 143 | }; 144 | 145 | return { 146 | init: init, 147 | goNext: goNext, 148 | goPrev: goPrev 149 | }; 150 | }); 151 | 152 | -------------------------------------------------------------------------------- /theme/javascript/core/progress.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "lodash", 3 | "jQuery", 4 | "utils/storage", 5 | "core/state" 6 | ], function(_, $, storage, state) { 7 | // Get current level 8 | var getCurrentLevel = function() { 9 | return state.level; 10 | }; 11 | 12 | // Return all levels 13 | var getLevels = function () { 14 | var levels = $(".book-summary li[data-level]"); 15 | 16 | return _.map(levels, function(level) { 17 | return $(level).data("level").toString(); 18 | }); 19 | }; 20 | 21 | // Return a map chapter -> number (timestamp) 22 | var getProgress = function () { 23 | // Current level 24 | var progress = storage.get("progress", {}); 25 | 26 | // Levels 27 | var levels = getLevels(); 28 | 29 | _.each(levels, function(level) { 30 | progress[level] = progress[level] || 0; 31 | }); 32 | 33 | return progress; 34 | }; 35 | 36 | // Change value of progress for a level 37 | var markProgress = function (level, state) { 38 | var progress = getProgress(); 39 | 40 | if (state == null) { 41 | state = true; 42 | } 43 | 44 | progress[level] = state 45 | ? Date.now() 46 | : 0; 47 | 48 | storage.set("progress", progress); 49 | }; 50 | 51 | // Show progress 52 | var showProgress = function () { 53 | var progress = getProgress(); 54 | var $summary = $(".book-summary"); 55 | 56 | _.each(progress, function (value, level) { 57 | $summary.find("li[data-level='"+level+"']").toggleClass("done", value > 0); 58 | }); 59 | 60 | // Mark current progress if we have not already 61 | if (!progress[getCurrentLevel()]) { 62 | markProgress(getCurrentLevel(), true); 63 | } 64 | }; 65 | 66 | return { 67 | 'current': getCurrentLevel, 68 | 'levels': getLevels, 69 | 'get': getProgress, 70 | 'mark': markProgress, 71 | 'show': showProgress 72 | }; 73 | }); -------------------------------------------------------------------------------- /theme/javascript/core/search.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "lodash", 4 | "lunr", 5 | "utils/storage", 6 | "utils/highlight", 7 | "core/state", 8 | "core/sidebar" 9 | ], function($, _, lunr, storage, highlight, state, sidebar) { 10 | var index = null; 11 | 12 | // Use a specific idnex 13 | var useIndex = function(data) { 14 | index = lunr.Index.load(data); 15 | }; 16 | 17 | // Load complete index 18 | var loadIndex = function() { 19 | $.getJSON(state.basePath+"/search_index.json") 20 | .then(useIndex); 21 | }; 22 | 23 | // Search for a term 24 | var search = function(q) { 25 | if (!index) return; 26 | var results = _.chain(index.search(q)) 27 | .map(function(result) { 28 | var parts = result.ref.split("#") 29 | return { 30 | path: parts[0], 31 | hash: parts[1] 32 | } 33 | }) 34 | .value(); 35 | 36 | return results; 37 | }; 38 | 39 | // Toggle search bar 40 | var toggleSearch = function(_state) { 41 | if (state != null && isSearchOpen() == _state) return; 42 | 43 | var $searchInput = $(".book-search input"); 44 | state.$book.toggleClass("with-search", _state); 45 | 46 | // If search bar is open: focus input 47 | if (isSearchOpen()) { 48 | sidebar.toggle(true); 49 | $searchInput.focus(); 50 | } else { 51 | $searchInput.blur(); 52 | $searchInput.val(""); 53 | sidebar.filter(null); 54 | highlight.clearHighlight(); 55 | storage.remove("keyword"); 56 | } 57 | }; 58 | 59 | // Return true if search bar is open 60 | var isSearchOpen = function() { 61 | return state.$book.hasClass("with-search"); 62 | }; 63 | 64 | 65 | var init = function() { 66 | loadIndex(); 67 | 68 | // Toggle search 69 | $(document).on("click", ".book-header .toggle-search", function(e) { 70 | e.preventDefault(); 71 | toggleSearch(); 72 | }); 73 | 74 | 75 | // Type in search bar 76 | $(document).on("keyup", ".book-search input", function(e) { 77 | var key = (e.keyCode ? e.keyCode : e.which); 78 | var q = $(this).val(); 79 | 80 | if (key == 27) { 81 | e.preventDefault(); 82 | toggleSearch(false); 83 | return; 84 | } 85 | if (q.length == 0) { 86 | sidebar.filter(null); 87 | storage.remove("keyword"); 88 | } else { 89 | var results = search(q); 90 | sidebar.filter( 91 | _.pluck(results, "path") 92 | ); 93 | storage.set("keyword", q); 94 | highlight.highlight($('div.book-body')[0], q); 95 | } 96 | }) 97 | 98 | }; 99 | 100 | // filter sidebar menu with current search keyword 101 | var recoverSearch = function() { 102 | var keyword = storage.get("keyword", ""); 103 | if(keyword.length > 0) { 104 | if(!isSearchOpen()) { 105 | toggleSearch(); 106 | } 107 | sidebar.filter(_.pluck(search(keyword), "path")); 108 | } 109 | $(".book-search input").val(keyword); 110 | highlight.highlight($('div.book-body')[0], keyword); 111 | }; 112 | 113 | return { 114 | init: init, 115 | search: search, 116 | toggle: toggleSearch, 117 | recover:recoverSearch 118 | }; 119 | }); 120 | -------------------------------------------------------------------------------- /theme/javascript/core/sidebar.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "lodash", 4 | "utils/storage", 5 | "utils/platform", 6 | "core/state" 7 | ], function($, _, storage, platform, state) { 8 | // Toggle sidebar with or withour animation 9 | var toggleSidebar = function(_state, animation) { 10 | if (state != null && isOpen() == _state) return; 11 | if (animation == null) animation = true; 12 | 13 | state.$book.toggleClass("without-animation", !animation); 14 | state.$book.toggleClass("with-summary", _state); 15 | 16 | storage.set("sidebar", isOpen()); 17 | }; 18 | 19 | // Return true if sidebar is open 20 | var isOpen = function() { 21 | return state.$book.hasClass("with-summary"); 22 | }; 23 | 24 | // Prepare sidebar: state and toggle button 25 | var init = function() { 26 | // Toggle summary 27 | $(document).on("click", ".book-header .toggle-summary", function(e) { 28 | e.preventDefault(); 29 | toggleSidebar(); 30 | }); 31 | 32 | // Init last state if not mobile 33 | if (!platform.isMobile) { 34 | toggleSidebar(storage.get("sidebar", true), false); 35 | } 36 | }; 37 | 38 | // Filter summary with a list of path 39 | var filterSummary = function(paths) { 40 | var $summary = $(".book-summary"); 41 | 42 | $summary.find("li").each(function() { 43 | var path = $(this).data("path"); 44 | var st = paths == null || _.contains(paths, path); 45 | 46 | $(this).toggle(st); 47 | if (st) $(this).parents("li").show(); 48 | }); 49 | }; 50 | 51 | return { 52 | init: init, 53 | toggle: toggleSidebar, 54 | filter: filterSummary 55 | } 56 | }); -------------------------------------------------------------------------------- /theme/javascript/core/state.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery" 3 | ], function() { 4 | var state = {}; 5 | 6 | state.update = function(dom) { 7 | var $book = $(dom.find(".book")); 8 | 9 | state.$book = $book; 10 | state.level = $book.data("level"); 11 | state.basePath = $book.data("basepath"); 12 | state.revision = $book.data("revision"); 13 | }; 14 | 15 | state.update($); 16 | 17 | return state; 18 | }); -------------------------------------------------------------------------------- /theme/javascript/gitbook.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "utils/storage", 4 | "utils/sharing", 5 | "utils/dropdown", 6 | 7 | "core/events", 8 | "core/font-settings", 9 | "core/state", 10 | "core/keyboard", 11 | "core/navigation", 12 | "core/progress", 13 | "core/sidebar", 14 | "core/search", 15 | "core/glossary" 16 | ], function($, storage, sharing, dropdown, events, fontSettings, state, keyboard, navigation, progress, sidebar, search, glossary){ 17 | var start = function(config) { 18 | var $book; 19 | $book = state.$book; 20 | 21 | // Init sidebar 22 | sidebar.init(); 23 | 24 | // Load search 25 | search.init(); 26 | 27 | // Load glossary 28 | glossary.init(); 29 | 30 | // Init keyboard 31 | keyboard.init(); 32 | 33 | // Bind sharing button 34 | sharing.init(); 35 | 36 | // Bind dropdown 37 | dropdown.init(); 38 | 39 | // Init navigation 40 | navigation.init(); 41 | 42 | //Init font settings 43 | fontSettings.init(config.fontSettings || {}); 44 | 45 | events.trigger("start", config); 46 | } 47 | 48 | return { 49 | start: start, 50 | events: events, 51 | state: state 52 | }; 53 | }); 54 | -------------------------------------------------------------------------------- /theme/javascript/utils/dropdown.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery" 3 | ], function($) { 4 | 5 | var toggleDropdown = function(e) { 6 | var $dropdown = $(e.currentTarget).parent().find(".dropdown-menu"); 7 | 8 | $dropdown.toggleClass("open"); 9 | e.stopPropagation(); 10 | e.preventDefault(); 11 | }; 12 | 13 | var closeDropdown = function(e) { 14 | $(".dropdown-menu").removeClass("open"); 15 | }; 16 | 17 | // Bind all dropdown 18 | var init = function() { 19 | $(document).on('click', ".toggle-dropdown", toggleDropdown); 20 | $(document).on('click', ".dropdown-menu", function(e){ e.stopPropagation(); }); 21 | $(document).on("click", closeDropdown); 22 | }; 23 | 24 | return { 25 | init: init 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /theme/javascript/utils/highlight.js: -------------------------------------------------------------------------------- 1 | // based on http://bartaz.github.io/sandbox.js/jquery.highlight.html#how 2 | // only do little customisation 3 | define([ 4 | "jQuery" 5 | ], function($) { 6 | 7 | var defaultWrapNodeName = "span"; 8 | var defaultHighlightClass = "book-search-highlight"; 9 | 10 | var highlight = function(node, keyword, wrapNodeName, className) { 11 | var regex = generateRegex(keyword); 12 | if (typeof node === 'undefined' || regex === null) { 13 | return; 14 | } 15 | wrapNodeName = wrapNodeName || defaultWrapNodeName; 16 | className = className || defaultHighlightClass; 17 | highlightText(node, regex); 18 | 19 | function highlightText(node, regex) { 20 | if (node.nodeType === 3) { 21 | var highlightElement = document.createElement(wrapNodeName); 22 | highlightElement.className = className; 23 | var match = regex.exec(node.data); 24 | if(match) { 25 | // split text nodes 26 | node.splitText(match.index + match[0].length); 27 | var matchedTextNode = node.splitText(match.index); 28 | var cloneMatchedTextNode = matchedTextNode.cloneNode(true); 29 | // wrap matched text node 30 | highlightElement.appendChild(cloneMatchedTextNode); 31 | matchedTextNode.parentNode.replaceChild(highlightElement, matchedTextNode); 32 | return 1; 33 | } 34 | } else if ((node.nodeType === 1 && node.childNodes) && !/(script|style)/i.test(node.tagName)) { 35 | for(var index = 0; index < node.childNodes.length; index++) { 36 | if (!(node.tagName === wrapNodeName.toUpperCase() && node.className === className)) { 37 | // skip new node created by text nodes splitting 38 | index += highlightText(node.childNodes[index], regex, wrapNodeName, className); 39 | } 40 | } 41 | } 42 | return 0; 43 | } 44 | 45 | function generateRegex(keyword) { 46 | var words = []; 47 | if (keyword.constructor === String) { 48 | words = keyword.split(' '); 49 | } else { 50 | return null; 51 | } 52 | // filter empty string 53 | words = $.grep(words, function(value, index) { 54 | return value !== ''; 55 | }); 56 | // escape reserved word 57 | words = jQuery.map(words, function(value, index) { 58 | return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 59 | }); 60 | if (words.length === 0) { 61 | return null; 62 | } 63 | var pattern = "(?:" + words.join("|") + ")"; 64 | return new RegExp(pattern, "i"); 65 | } 66 | }; 67 | 68 | var clearHighlight = function(wrapNodeName, className) { 69 | wrapNodeName = wrapNodeName || defaultWrapNodeName; 70 | className = className || defaultHighlightClass; 71 | return $(wrapNodeName + "." + className).each(function () { 72 | var parent = this.parentNode; 73 | parent.replaceChild(this.firstChild, this); 74 | parent.normalize(); 75 | }).end(); 76 | }; 77 | 78 | return { 79 | highlight: highlight, 80 | clearHighlight: clearHighlight 81 | }; 82 | }); 83 | -------------------------------------------------------------------------------- /theme/javascript/utils/platform.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | return { 3 | isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) 4 | }; 5 | }); -------------------------------------------------------------------------------- /theme/javascript/utils/sharing.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jQuery", 3 | "utils/qrcode" 4 | ], function($, qrcode) { 5 | var qrcodeInfo = { 6 | width : 128, 7 | height : 128, 8 | colorDark : "#000000", 9 | colorLight : "#ffffff", 10 | correctLevel : 2 11 | }; 12 | 13 | var types = { 14 | "twitter": function($el) { 15 | window.open("http://twitter.com/home?status="+encodeURIComponent($("title").text()+" "+location.href)) 16 | }, 17 | "facebook": function($el) { 18 | window.open("http://www.facebook.com/sharer/sharer.php?s=100&p[url]="+encodeURIComponent(location.href)) 19 | }, 20 | "google-plus": function($el) { 21 | window.open("https://plus.google.com/share?url="+encodeURIComponent(location.href)) 22 | }, 23 | "weibo": function($el) { 24 | window.open("http://service.weibo.com/share/share.php?content=utf-8&url="+encodeURIComponent(location.href)+"&title="+encodeURIComponent($("title").text())) 25 | }, 26 | "qq": function($el) { 27 | var title = $("title").text(); 28 | window.open("http://connect.qq.com/widget/shareqq/index.html?content=utf-8&url="+encodeURIComponent(location.href)+"&title="+encodeURIComponent(title)+"&desc="+encodeURIComponent(title)) 29 | }, 30 | "qrcode": function($el) { 31 | popQRCode(); 32 | }, 33 | "instapaper": function($el) { 34 | window.open("http://www.instapaper.com/text?u="+encodeURIComponent(location.href)); 35 | } 36 | }; 37 | 38 | // Bind all sharing button 39 | var init = function() { 40 | $(document).on("click", "a[data-sharing],button[data-sharing]", function(e) { 41 | if (e) { 42 | e.preventDefault(); 43 | e.stopPropagation(); 44 | } 45 | var type = $(this).data("sharing"); 46 | 47 | types[type]($(this)); 48 | }); 49 | }; 50 | 51 | var popQRCode = function() { 52 | var qrcodeDropdown = $("#dropdown-qrcode"); 53 | if (qrcodeDropdown.hasClass("open")) { 54 | qrcodeDropdown.removeClass("open"); 55 | } else { 56 | var current_url = location.href; 57 | var lastUrl = $("#last_url"); 58 | if (lastUrl[0] && lastUrl.val() !== current_url) { 59 | qrcodeInfo.text = current_url; 60 | new qrcode.qrcode($("#qrcode")[0], qrcodeInfo); 61 | } 62 | qrcodeDropdown.addClass("open"); 63 | lastUrl.val(current_url); 64 | } 65 | }; 66 | 67 | return { 68 | init: init 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /theme/javascript/utils/storage.js: -------------------------------------------------------------------------------- 1 | define(function(){ 2 | var baseKey = ""; 3 | 4 | /* 5 | * Simple module for storing data in the browser's local storage 6 | */ 7 | return { 8 | setBaseKey: function(key) { 9 | baseKey = key; 10 | }, 11 | set: function(key, value) { 12 | key = baseKey+":"+key; 13 | localStorage[key] = JSON.stringify(value); 14 | }, 15 | get: function(key, def) { 16 | key = baseKey+":"+key; 17 | if (localStorage[key] === undefined) return def; 18 | try { 19 | var v = JSON.parse(localStorage[key]); 20 | return v == null ? def : v;; 21 | } catch(err) { 22 | console.error(err); 23 | return localStorage[key] || def; 24 | } 25 | }, 26 | remove: function(key) { 27 | key = baseKey+":"+key; 28 | localStorage.removeItem(key); 29 | } 30 | }; 31 | }); -------------------------------------------------------------------------------- /theme/javascript/utils/url.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "URIjs/URI" 3 | ], function(URI) { 4 | // Joins path segments. Preserves initial "/" and resolves ".." and "." 5 | // Does not support using ".." to go above/outside the root. 6 | // This means that join("foo", "../../bar") will not resolve to "../bar" 7 | function join(baseUrl, url) { 8 | var theUrl = new URI(url); 9 | if (theUrl.is("relative")) { 10 | theUrl = theUrl.absoluteTo(baseUrl); 11 | } 12 | return theUrl.toString(); 13 | } 14 | 15 | // A simple function to get the dirname of a path 16 | // Trailing slashes are ignored. Leading slash is preserved. 17 | function dirname(path) { 18 | return join(path, ".."); 19 | } 20 | 21 | // test if a path or url is absolute 22 | function isAbsolute(path) { 23 | if (!path) return false; 24 | 25 | return (path[0] == "/" || path.indexOf("http://") == 0 || path.indexOf("https://") == 0); 26 | } 27 | 28 | return { 29 | dirname: dirname, 30 | join: join, 31 | isAbsolute: isAbsolute 32 | }; 33 | }) -------------------------------------------------------------------------------- /theme/stylesheets/ebook.less: -------------------------------------------------------------------------------- 1 | @import "mixins.less"; 2 | 3 | @import "ebook/variables.less"; 4 | @import "ebook/highlight.less"; 5 | 6 | .page { 7 | &.page-toc { 8 | ol { 9 | margin: 0px; 10 | } 11 | } 12 | 13 | .book-chapter { 14 | display: none; 15 | } 16 | 17 | .exercise, .quiz { 18 | margin: 1cm 0cm; 19 | border: 2px solid #ddd; 20 | 21 | .exercise-header { 22 | padding: 0.1cm 0.3cm; 23 | background: #f5f5f5; 24 | border-bottom: 1px solid #ddd; 25 | font-weight: bold; 26 | page-break-inside: avoid; 27 | } 28 | 29 | .exercise-inner { 30 | padding: 0.15cm 0.3cm; 31 | 32 | p:last-child { 33 | margin: 0px; 34 | } 35 | } 36 | 37 | hr { 38 | height: 2px; 39 | } 40 | 41 | .question { 42 | margin-top: 0.1cm; 43 | border-top: 1px solid #ddd; 44 | 45 | .question-base { 46 | 47 | } 48 | 49 | .question-solution { 50 | 51 | } 52 | } 53 | } 54 | } 55 | 56 | 57 | 58 | body { 59 | .page { 60 | font-family: sans-serif; 61 | .markdown-content(#333, 1.6); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /theme/stylesheets/ebook/highlight.less: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.io/color-themes-for-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-title { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-attribute, 12 | .hljs-tag, 13 | .hljs-regexp, 14 | .ruby .hljs-constant, 15 | .xml .hljs-tag .hljs-title, 16 | .xml .hljs-pi, 17 | .xml .hljs-doctype, 18 | .html .hljs-doctype, 19 | .css .hljs-id, 20 | .css .hljs-class, 21 | .css .hljs-pseudo { 22 | color: #c82829; 23 | } 24 | 25 | /* Tomorrow Orange */ 26 | .hljs-number, 27 | .hljs-preprocessor, 28 | .hljs-pragma, 29 | .hljs-built_in, 30 | .hljs-literal, 31 | .hljs-params, 32 | .hljs-constant { 33 | color: #f5871f; 34 | } 35 | 36 | /* Tomorrow Yellow */ 37 | .ruby .hljs-class .hljs-title, 38 | .css .hljs-rules .hljs-attribute { 39 | color: #eab700; 40 | } 41 | 42 | /* Tomorrow Green */ 43 | .hljs-string, 44 | .hljs-value, 45 | .hljs-inheritance, 46 | .hljs-header, 47 | .ruby .hljs-symbol, 48 | .xml .hljs-cdata { 49 | color: #718c00; 50 | } 51 | 52 | /* Tomorrow Aqua */ 53 | .css .hljs-hexcolor { 54 | color: #3e999f; 55 | } 56 | 57 | /* Tomorrow Blue */ 58 | .hljs-function, 59 | .python .hljs-decorator, 60 | .python .hljs-title, 61 | .ruby .hljs-function .hljs-title, 62 | .ruby .hljs-title .hljs-keyword, 63 | .perl .hljs-sub, 64 | .javascript .hljs-title, 65 | .coffeescript .hljs-title { 66 | color: #4271ae; 67 | } 68 | 69 | /* Tomorrow Purple */ 70 | .hljs-keyword, 71 | .javascript .hljs-function { 72 | color: #8959a8; 73 | } 74 | 75 | .hljs { 76 | display: block; 77 | background: white; 78 | color: #4d4d4c; 79 | padding: 0.5em; 80 | } 81 | 82 | .coffeescript .javascript, 83 | .javascript .xml, 84 | .tex .hljs-formula, 85 | .xml .javascript, 86 | .xml .vbscript, 87 | .xml .css, 88 | .xml .hljs-cdata { 89 | opacity: 0.5; 90 | } 91 | -------------------------------------------------------------------------------- /theme/stylesheets/ebook/variables.less: -------------------------------------------------------------------------------- 1 | @font-size-base: 13px; 2 | -------------------------------------------------------------------------------- /theme/stylesheets/mixins.less: -------------------------------------------------------------------------------- 1 | @import "base/markdown.less"; 2 | 3 | .link-inherit { 4 | color: inherit; 5 | 6 | &:hover, &:focus { 7 | color: inherit; 8 | } 9 | } 10 | 11 | .transition-transform(@transition) { 12 | -webkit-transition: -webkit-transform @transition; 13 | -moz-transition: -moz-transform @transition; 14 | -o-transition: -o-transform @transition; 15 | transition: transform @transition; 16 | } 17 | 18 | .hidden { 19 | display: none; 20 | } 21 | 22 | .clearfix { 23 | *zoom: 1; 24 | 25 | &:before, &:after { 26 | content: ""; 27 | display: table; 28 | } 29 | 30 | &:after { 31 | clear: both; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /theme/stylesheets/website.less: -------------------------------------------------------------------------------- 1 | @import "base/normalize.less"; 2 | @import "base/preboot.less"; 3 | @import "../vendors/fontawesome/less/font-awesome.less"; 4 | 5 | @import "mixins.less"; 6 | 7 | @import "website/fonts.less"; 8 | @import "website/variables.less"; 9 | @import "website/languages.less"; 10 | @import "website/header.less"; 11 | @import "website/dropdown.less"; 12 | @import "website/alerts.less"; 13 | @import "website/summary.less"; 14 | @import "website/font-settings.less"; 15 | @import "website/body.less"; 16 | @import "website/buttons.less"; 17 | @import "website/markdown.less"; 18 | @import "website/navigation.less"; 19 | @import "website/glossary.less"; 20 | @import "website/search-highlight.less"; 21 | 22 | * { 23 | .box-sizing(border-box); 24 | -webkit-overflow-scrolling: touch; 25 | -webkit-tap-highlight-color: transparent; 26 | -webkit-text-size-adjust: none; 27 | -webkit-touch-callout: none; 28 | -webkit-font-smoothing: antialiased; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | } 34 | 35 | html, body { 36 | height: 100%; 37 | overflow-y: hidden; 38 | } 39 | 40 | html { 41 | font-size: 62.5%; 42 | } 43 | 44 | body { 45 | text-rendering: optimizeLegibility; 46 | font-smoothing: antialiased; 47 | font-family: @font-family-base; 48 | font-size: @font-size-base; 49 | } 50 | -------------------------------------------------------------------------------- /theme/stylesheets/website/alerts.less: -------------------------------------------------------------------------------- 1 | .alert { 2 | padding: 15px; 3 | margin-bottom: 20px; 4 | color: #444; 5 | background: #eee; 6 | border-bottom: 5px solid #ddd; 7 | } 8 | 9 | .alert-success { 10 | background: @state-success-bg; 11 | border-color: @state-success-border; 12 | color: @state-success-text; 13 | } 14 | 15 | .alert-info { 16 | background: @state-info-bg; 17 | border-color: @state-info-border; 18 | color: @state-info-text; 19 | } 20 | 21 | .alert-danger { 22 | background: @state-danger-bg; 23 | border-color: @state-danger-border; 24 | color: @state-danger-text; 25 | } 26 | 27 | .alert-warning { 28 | background: @state-warning-bg; 29 | border-color: @state-warning-border; 30 | color: @state-warning-text; 31 | } 32 | -------------------------------------------------------------------------------- /theme/stylesheets/website/body.less: -------------------------------------------------------------------------------- 1 | .book { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | 6 | .book-body { 7 | position: absolute; 8 | top: 0px; 9 | right: 0px; 10 | left: 0px; 11 | bottom: 0px; 12 | overflow-y: auto; 13 | 14 | color: @page-color; 15 | background: @body-background; 16 | .transition(left @sidebar-transition-duration ease); 17 | 18 | .body-inner { 19 | position: absolute; 20 | top: 0px; 21 | right: 0px; 22 | left: 0px; 23 | bottom: 0px; 24 | overflow-y: auto; 25 | } 26 | 27 | .page-wrapper { 28 | position: relative; 29 | outline: none; 30 | 31 | .page-inner { 32 | max-width: 800px; 33 | margin: 0px auto; 34 | padding: 20px 0px 40px 0px; 35 | 36 | section { 37 | margin: 0px 0px; 38 | padding: 5px 15px; 39 | 40 | background: @page-background; 41 | border-radius: 2px; 42 | line-height: @content-line-height; 43 | font-size: @default-font-size; 44 | } 45 | 46 | .btn-group { 47 | .btn { 48 | border-radius: 0px; 49 | background: #eee; 50 | border: 0px; 51 | } 52 | } 53 | } 54 | } 55 | 56 | @media (max-width: @mobileMaxWidth) { 57 | .transition-transform(@sidebar-transition-duration ease); 58 | padding-bottom: 20px; 59 | 60 | .body-inner { 61 | position: static; 62 | min-height: calc(~"100% - 50px") 63 | } 64 | } 65 | } 66 | 67 | &.with-summary { 68 | @media (min-width: @sidebar-breakpoint) { 69 | .book-body { 70 | left: @sidebar-width; 71 | } 72 | } 73 | @media (max-width: @sidebar-breakpoint) { 74 | overflow: hidden; 75 | 76 | .book-body { 77 | .translate(~"calc(100% - 60px)", 0px); 78 | } 79 | } 80 | } 81 | 82 | &.without-animation { 83 | .book-body { 84 | .transition(none) !important; 85 | } 86 | } 87 | &.color-theme-1{ 88 | .book-body { 89 | color: @page-color-1; 90 | background: @body-background-1; 91 | .page-wrapper { 92 | .page-inner { 93 | section{ 94 | background: @page-background-1; 95 | } 96 | } 97 | } 98 | } 99 | } 100 | &.color-theme-2{ 101 | .book-body { 102 | color: @page-color-2; 103 | background: @body-background-2; 104 | .page-wrapper { 105 | .page-inner { 106 | section{ 107 | background: @page-background-2; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | &.font-size-0 { 115 | .book-body .page-inner section { 116 | font-size:@s-font-size; 117 | } 118 | } 119 | &.font-size-1 { 120 | .book-body .page-inner section { 121 | font-size:@m-font-size; 122 | } 123 | } 124 | &.font-size-2 { 125 | .book-body .page-inner section { 126 | font-size:@l-font-size; 127 | } 128 | } 129 | &.font-size-3{ 130 | .book-body .page-inner section { 131 | font-size:@xl-font-size; 132 | } 133 | } 134 | &.font-size-4 { 135 | .book-body .page-inner section { 136 | font-size:@xxl-font-size; 137 | } 138 | } 139 | 140 | &.font-family-0{ 141 | font-family: @font-family-serif; 142 | } 143 | &.font-family-1{ 144 | font-family: @font-family-sans; 145 | } 146 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/buttons.less: -------------------------------------------------------------------------------- 1 | .buttons { 2 | .clearfix(); 3 | } 4 | 5 | .button { 6 | border: 0; 7 | background-color: transparent; 8 | background: @button-background; 9 | color: @button-color; 10 | width: 100%; 11 | text-align: center; 12 | float: left; 13 | line-height: @line-height-base; 14 | padding: 8px 4px; 15 | 16 | &:hover { 17 | color: @button-hover-color; 18 | } 19 | 20 | &:focus, &:hover { 21 | outline: none; 22 | } 23 | 24 | &.size-2 { 25 | width: 50%; 26 | } 27 | 28 | &.size-3 { 29 | width: 33%; 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /theme/stylesheets/website/dropdown.less: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: relative; 3 | } 4 | 5 | .dropdown-menu { 6 | position: absolute; 7 | top: 100%; 8 | left: 0; 9 | z-index: @dropdown-zindex; 10 | display: none; // none by default, but block on "open" of the menu 11 | float: left; 12 | min-width: 160px; 13 | padding: 0; 14 | margin: 2px 0 0; // override default ul 15 | list-style: none; 16 | font-size: @font-size-base; 17 | background-color: @dropdown-background; 18 | border: 1px solid @dropdown-border-color; 19 | border-radius: @border-radius-base; 20 | .box-shadow(0 6px 12px rgba(0,0,0,.175)); 21 | background-clip: padding-box; 22 | 23 | &.open{ 24 | display:block; 25 | } 26 | 27 | &.dropdown-left { 28 | left: auto; 29 | right: 4%; 30 | 31 | .dropdown-caret { 32 | right: 14px; 33 | left: auto; 34 | } 35 | } 36 | 37 | .dropdown-caret{ 38 | position: absolute; 39 | top: -8px; 40 | left: 14px; 41 | width: 18px; 42 | height: 10px; 43 | float: left; 44 | overflow: hidden; 45 | 46 | .caret-outer{ 47 | position: absolute; 48 | border-left: 9px solid transparent; 49 | border-right: 9px solid transparent; 50 | border-bottom: 9px solid rgba(0,0,0,0.1); 51 | height: auto; 52 | left: 0; 53 | top: 0; 54 | width: auto; 55 | display: inline-block; 56 | margin-left: -1px; 57 | } 58 | .caret-inner{ 59 | position: absolute; 60 | display: inline-block; 61 | margin-top: -1px; 62 | top: 0; 63 | top: 1px; 64 | border-left: 9px solid transparent; 65 | border-right: 9px solid transparent; 66 | border-bottom: 9px solid @dropdown-background; 67 | } 68 | } 69 | 70 | .buttons { 71 | .clearfix(); 72 | border-bottom: 1px solid @sidebar-divider-color; 73 | 74 | &:last-child { 75 | border-bottom: none; 76 | } 77 | 78 | .button { 79 | border: 0; 80 | background-color: transparent; 81 | color: @dropdown-button-color; 82 | width: 100%; 83 | text-align: center; 84 | float: left; 85 | line-height: @line-height-base; 86 | padding: 8px 4px; 87 | 88 | &:hover { 89 | color: @dropdown-button-hover-color; 90 | } 91 | 92 | &:focus, &:hover { 93 | outline: none; 94 | } 95 | 96 | &.size-2 { 97 | width: 50%; 98 | } 99 | 100 | &.size-3 { 101 | width: 33%; 102 | } 103 | } 104 | } 105 | 106 | .qrcode { 107 | padding: 17px; 108 | background-color: @dropdown-background; 109 | } 110 | } 111 | 112 | /* 113 | * Theme 1 114 | */ 115 | 116 | .color-theme-1 { 117 | .dropdown-menu{ 118 | background-color: @dropdown-background-1; 119 | border-color: @dropdown-border-color-1; 120 | 121 | .dropdown-caret .caret-inner{ 122 | border-bottom: 9px solid @dropdown-background-1; 123 | } 124 | 125 | .buttons { 126 | border-color: @dropdown-divider-color-1; 127 | } 128 | 129 | .button { 130 | color: @dropdown-button-color-1; 131 | 132 | &:hover{ 133 | color: @dropdown-button-hover-color-1; 134 | } 135 | } 136 | 137 | .qrcode { 138 | padding: 17px; 139 | background-color: @dropdown-background; 140 | } 141 | } 142 | } 143 | 144 | /* 145 | * Theme 2 146 | */ 147 | 148 | .color-theme-2 { 149 | .dropdown-menu{ 150 | background-color: @dropdown-background-2; 151 | border-color: @dropdown-border-color-2; 152 | 153 | .dropdown-caret .caret-inner{ 154 | border-bottom: 9px solid @dropdown-background-2; 155 | } 156 | 157 | .buttons { 158 | border-color: @dropdown-divider-color-2; 159 | } 160 | 161 | .button { 162 | color: @dropdown-button-color-2; 163 | 164 | &:hover{ 165 | color: @dropdown-button-hover-color-2; 166 | } 167 | } 168 | 169 | .qrcode { 170 | padding: 17px; 171 | background-color: @dropdown-background; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /theme/stylesheets/website/font-settings.less: -------------------------------------------------------------------------------- 1 | .book-header{ 2 | #font-settings-wrapper{ 3 | #enlarge-font-size, #reduce-font-size { 4 | line-height: 30px; 5 | } 6 | 7 | #enlarge-font-size{ 8 | font-size: 1.4em; 9 | } 10 | #reduce-font-size{ 11 | font-size: 1em; 12 | } 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /theme/stylesheets/website/fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Merriweather'; 3 | font-style: normal; 4 | font-weight: 250; 5 | src: local('Merriweather Light'),url('@{FontPath}/merriweather/250.woff') format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Merriweather'; 9 | font-style: italic; 10 | font-weight: 250; 11 | src: local('Merriweather Light Italic'),url('@{FontPath}/merriweather/250i.woff') format('woff'); 12 | } 13 | @font-face { 14 | font-family: 'Merriweather'; 15 | font-style: normal; 16 | font-weight: 400; 17 | src: local('Merriweather'),url('@{FontPath}/merriweather/400.woff') format('woff'); 18 | } 19 | @font-face { 20 | font-family: 'Merriweather'; 21 | font-style: italic; 22 | font-weight: 400; 23 | src: local('Merriweather Italic'),url('@{FontPath}/merriweather/400i.woff') format('woff'); 24 | } 25 | @font-face { 26 | font-family: 'Merriweather'; 27 | font-style: normal; 28 | font-weight: 700; 29 | src: local('Merriweather Bold'),url('@{FontPath}/merriweather/700.woff') format('woff'); 30 | } 31 | @font-face { 32 | font-family: 'Merriweather'; 33 | font-style: italic; 34 | font-weight: 700; 35 | src: local('Merriweather Bold Italic'),url('@{FontPath}/merriweather/700i.woff') format('woff'); 36 | } 37 | @font-face { 38 | font-family: 'Merriweather'; 39 | font-style: normal; 40 | font-weight: 900; 41 | src: local('Merriweather Heavy'),url('@{FontPath}/merriweather/900.woff') format('woff'); 42 | } 43 | @font-face { 44 | font-family: 'Merriweather'; 45 | font-style: italic; 46 | font-weight: 900; 47 | src: local('Merriweather Heavy Italic'),url('@{FontPath}/merriweather/900i.woff') format('woff'); 48 | } 49 | @font-face { 50 | font-family: 'Open Sans'; 51 | font-style: normal; 52 | font-weight: 300; 53 | src: local('Open Sans Light'),url('@{FontPath}/opensans/300.woff') format('woff'); 54 | } 55 | @font-face { 56 | font-family: 'Open Sans'; 57 | font-style: italic; 58 | font-weight: 300; 59 | src: local('Open Sans Light Italic'),url('@{FontPath}/opensans/300i.woff') format('woff'); 60 | } 61 | @font-face { 62 | font-family: 'Open Sans'; 63 | font-style: normal; 64 | font-weight: 400; 65 | src: local('Open Sans Regular'),url('@{FontPath}/opensans/400.woff') format('woff'); 66 | } 67 | @font-face { 68 | font-family: 'Open Sans'; 69 | font-style: italic; 70 | font-weight: 400; 71 | src: local('Open Sans Italic'),url('@{FontPath}/opensans/400i.woff') format('woff'); 72 | } 73 | @font-face { 74 | font-family: 'Open Sans'; 75 | font-style: normal; 76 | font-weight: 600; 77 | src: local('Open Sans Semibold'),url('@{FontPath}/opensans/600.woff') format('woff'); 78 | } 79 | @font-face { 80 | font-family: 'Open Sans'; 81 | font-style: italic; 82 | font-weight: 600; 83 | src: local('Open Sans Semibold Italic'),url('@{FontPath}/opensans/600i.woff') format('woff'); 84 | } 85 | @font-face { 86 | font-family: 'Open Sans'; 87 | font-style: normal; 88 | font-weight: 700; 89 | src: local('Open Sans Bold'),url('@{FontPath}/opensans/700.woff') format('woff'); 90 | } 91 | @font-face { 92 | font-family: 'Open Sans'; 93 | font-style: italic; 94 | font-weight: 700; 95 | src: local('Open Sans Bold Italic'),url('@{FontPath}/opensans/700i.woff') format('woff'); 96 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/glossary.less: -------------------------------------------------------------------------------- 1 | .book .book-body .page-wrapper .page-inner section.glossary { 2 | margin-bottom: 40px; 3 | 4 | h2 { 5 | a, a:hover { 6 | color: inherit; 7 | text-decoration: none; 8 | } 9 | } 10 | 11 | .glossary-index { 12 | list-style: none; 13 | margin: 0px; 14 | padding: 0px; 15 | 16 | li { 17 | display: inline; 18 | margin: 0px 8px; 19 | white-space: nowrap; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /theme/stylesheets/website/header.less: -------------------------------------------------------------------------------- 1 | .book { 2 | .book-header { 3 | font-family: @font-family-sans; 4 | 5 | padding: 0px 8px; 6 | z-index: 2; 7 | 8 | font-size: 0.85em; 9 | color: @header-color; 10 | background: @header-background; 11 | 12 | .btn { 13 | display: block; 14 | height: @header-height; 15 | padding: 0px 15px; 16 | border-bottom: none; 17 | color: @header-button-color; 18 | text-transform: uppercase; 19 | line-height: @header-height; 20 | .box-shadow(none) !important; 21 | position:relative; 22 | font-size: @font-size-base; 23 | 24 | &:hover { 25 | position: relative; 26 | text-decoration: none; 27 | color: @header-button-hover-color; 28 | background: @header-button-hover-background; 29 | } 30 | } 31 | 32 | h1 { 33 | margin: 0px; 34 | font-size: 20px; 35 | font-weight: 200; 36 | text-align: center; 37 | line-height: @header-height; 38 | opacity: 0; 39 | .transition(opacity ease 0.4s); 40 | 41 | padding-left: 200px; 42 | padding-right: 200px; 43 | .transition(opacity 0.2s ease); 44 | 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | white-space: nowrap; 48 | 49 | a, a:hover { 50 | color: inherit; 51 | text-decoration: none; 52 | } 53 | 54 | @media (max-width: 1000px) { 55 | display: none; 56 | } 57 | 58 | i { 59 | display: none; 60 | } 61 | } 62 | 63 | &:hover { 64 | h1 { 65 | opacity: 1; 66 | } 67 | } 68 | } 69 | 70 | &.is-loading { 71 | .book-header h1 { 72 | i { 73 | display: inline-block; 74 | } 75 | a { 76 | display: none; 77 | } 78 | } 79 | } 80 | 81 | &.color-theme-1{ 82 | .book-header{ 83 | color: @header-color-1; 84 | background: @header-background-1; 85 | 86 | .btn { 87 | color: @header-button-color-1; 88 | &:hover { 89 | color: @header-button-hover-color-1; 90 | background: @header-button-hover-background-1; 91 | } 92 | } 93 | 94 | h1 { 95 | color: @page-color-1; 96 | } 97 | } 98 | } 99 | 100 | &.color-theme-2{ 101 | .book-header{ 102 | color: @header-color-2; 103 | background: @header-background-2; 104 | 105 | .btn { 106 | color: @header-button-color-2; 107 | &:hover { 108 | color: @header-button-hover-color-2; 109 | background: @header-button-hover-background-2; 110 | } 111 | } 112 | 113 | h1 { 114 | color: @page-color-2; 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /theme/stylesheets/website/highlight/night.less: -------------------------------------------------------------------------------- 1 | /* Tomorrow Night Bright Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 4 | 5 | /* Tomorrow Comment */ 6 | .hljs-comment, 7 | .hljs-title { 8 | color: #969896; 9 | } 10 | 11 | /* Tomorrow Red */ 12 | .hljs-variable, 13 | .hljs-attribute, 14 | .hljs-tag, 15 | .hljs-regexp, 16 | .ruby .hljs-constant, 17 | .xml .hljs-tag .hljs-title, 18 | .xml .hljs-pi, 19 | .xml .hljs-doctype, 20 | .html .hljs-doctype, 21 | .css .hljs-id, 22 | .css .hljs-class, 23 | .css .hljs-pseudo { 24 | color: #d54e53; 25 | } 26 | 27 | /* Tomorrow Orange */ 28 | .hljs-number, 29 | .hljs-preprocessor, 30 | .hljs-pragma, 31 | .hljs-built_in, 32 | .hljs-literal, 33 | .hljs-params, 34 | .hljs-constant { 35 | color: #e78c45; 36 | } 37 | 38 | /* Tomorrow Yellow */ 39 | .ruby .hljs-class .hljs-title, 40 | .css .hljs-rules .hljs-attribute { 41 | color: #e7c547; 42 | } 43 | 44 | /* Tomorrow Green */ 45 | .hljs-string, 46 | .hljs-value, 47 | .hljs-inheritance, 48 | .hljs-header, 49 | .ruby .hljs-symbol, 50 | .xml .hljs-cdata { 51 | color: #b9ca4a; 52 | } 53 | 54 | /* Tomorrow Aqua */ 55 | .css .hljs-hexcolor { 56 | color: #70c0b1; 57 | } 58 | 59 | /* Tomorrow Blue */ 60 | .hljs-function, 61 | .python .hljs-decorator, 62 | .python .hljs-title, 63 | .ruby .hljs-function .hljs-title, 64 | .ruby .hljs-title .hljs-keyword, 65 | .perl .hljs-sub, 66 | .javascript .hljs-title, 67 | .coffeescript .hljs-title { 68 | color: #7aa6da; 69 | } 70 | 71 | /* Tomorrow Purple */ 72 | .hljs-keyword, 73 | .javascript .hljs-function { 74 | color: #c397d8; 75 | } 76 | 77 | .hljs { 78 | display: block; 79 | background: black; 80 | color: #eaeaea; 81 | padding: 0.5em; 82 | } 83 | 84 | .coffeescript .javascript, 85 | .javascript .xml, 86 | .tex .hljs-formula, 87 | .xml .javascript, 88 | .xml .vbscript, 89 | .xml .css, 90 | .xml .hljs-cdata { 91 | opacity: 0.5; 92 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/highlight/sepia.less: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | padding: 0.5em; 10 | background: #fdf6e3; 11 | color: #657b83; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-template_comment, 16 | .diff .hljs-header, 17 | .hljs-doctype, 18 | .hljs-pi, 19 | .lisp .hljs-string, 20 | .hljs-javadoc { 21 | color: #93a1a1; 22 | } 23 | 24 | /* Solarized Green */ 25 | .hljs-keyword, 26 | .hljs-winutils, 27 | .method, 28 | .hljs-addition, 29 | .css .hljs-tag, 30 | .hljs-request, 31 | .hljs-status, 32 | .nginx .hljs-title { 33 | color: #859900; 34 | } 35 | 36 | /* Solarized Cyan */ 37 | .hljs-number, 38 | .hljs-command, 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-rules .hljs-value, 42 | .hljs-phpdoc, 43 | .tex .hljs-formula, 44 | .hljs-regexp, 45 | .hljs-hexcolor, 46 | .hljs-link_url { 47 | color: #2aa198; 48 | } 49 | 50 | /* Solarized Blue */ 51 | .hljs-title, 52 | .hljs-localvars, 53 | .hljs-chunk, 54 | .hljs-decorator, 55 | .hljs-built_in, 56 | .hljs-identifier, 57 | .vhdl .hljs-literal, 58 | .hljs-id, 59 | .css .hljs-function { 60 | color: #268bd2; 61 | } 62 | 63 | /* Solarized Yellow */ 64 | .hljs-attribute, 65 | .hljs-variable, 66 | .lisp .hljs-body, 67 | .smalltalk .hljs-number, 68 | .hljs-constant, 69 | .hljs-class .hljs-title, 70 | .hljs-parent, 71 | .haskell .hljs-type, 72 | .hljs-link_reference { 73 | color: #b58900; 74 | } 75 | 76 | /* Solarized Orange */ 77 | .hljs-preprocessor, 78 | .hljs-preprocessor .hljs-keyword, 79 | .hljs-pragma, 80 | .hljs-shebang, 81 | .hljs-symbol, 82 | .hljs-symbol .hljs-string, 83 | .diff .hljs-change, 84 | .hljs-special, 85 | .hljs-attr_selector, 86 | .hljs-subst, 87 | .hljs-cdata, 88 | .clojure .hljs-title, 89 | .css .hljs-pseudo, 90 | .hljs-header { 91 | color: #cb4b16; 92 | } 93 | 94 | /* Solarized Red */ 95 | .hljs-deletion, 96 | .hljs-important { 97 | color: #dc322f; 98 | } 99 | 100 | /* Solarized Violet */ 101 | .hljs-link_label { 102 | color: #6c71c4; 103 | } 104 | 105 | .tex .hljs-formula { 106 | background: #eee8d5; 107 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/highlight/white.less: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-title { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-attribute, 12 | .hljs-tag, 13 | .hljs-regexp, 14 | .ruby .hljs-constant, 15 | .xml .hljs-tag .hljs-title, 16 | .xml .hljs-pi, 17 | .xml .hljs-doctype, 18 | .html .hljs-doctype, 19 | .css .hljs-id, 20 | .css .hljs-class, 21 | .css .hljs-pseudo { 22 | color: #c82829; 23 | } 24 | 25 | /* Tomorrow Orange */ 26 | .hljs-number, 27 | .hljs-preprocessor, 28 | .hljs-pragma, 29 | .hljs-built_in, 30 | .hljs-literal, 31 | .hljs-params, 32 | .hljs-constant { 33 | color: #f5871f; 34 | } 35 | 36 | /* Tomorrow Yellow */ 37 | .ruby .hljs-class .hljs-title, 38 | .css .hljs-rules .hljs-attribute { 39 | color: #eab700; 40 | } 41 | 42 | /* Tomorrow Green */ 43 | .hljs-string, 44 | .hljs-value, 45 | .hljs-inheritance, 46 | .hljs-header, 47 | .ruby .hljs-symbol, 48 | .xml .hljs-cdata { 49 | color: #718c00; 50 | } 51 | 52 | /* Tomorrow Aqua */ 53 | .css .hljs-hexcolor { 54 | color: #3e999f; 55 | } 56 | 57 | /* Tomorrow Blue */ 58 | .hljs-function, 59 | .python .hljs-decorator, 60 | .python .hljs-title, 61 | .ruby .hljs-function .hljs-title, 62 | .ruby .hljs-title .hljs-keyword, 63 | .perl .hljs-sub, 64 | .javascript .hljs-title, 65 | .coffeescript .hljs-title { 66 | color: #4271ae; 67 | } 68 | 69 | /* Tomorrow Purple */ 70 | .hljs-keyword, 71 | .javascript .hljs-function { 72 | color: #8959a8; 73 | } 74 | 75 | .hljs { 76 | display: block; 77 | background: white; 78 | color: #4d4d4c; 79 | padding: 0.5em; 80 | } 81 | 82 | .coffeescript .javascript, 83 | .javascript .xml, 84 | .tex .hljs-formula, 85 | .xml .javascript, 86 | .xml .vbscript, 87 | .xml .css, 88 | .xml .hljs-cdata { 89 | opacity: 0.5; 90 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/languages.less: -------------------------------------------------------------------------------- 1 | .book-langs-index { 2 | width: 100%; 3 | height: 100%; 4 | padding: 40px 0px; 5 | margin: 0px; 6 | overflow: auto; 7 | 8 | @media (max-width: 600px) { 9 | padding: 0px; 10 | } 11 | 12 | .inner { 13 | max-width: 600px; 14 | width: 100%; 15 | 16 | margin: 0px auto; 17 | padding: 30px; 18 | 19 | background: #fff; 20 | border-radius: 3px; 21 | 22 | h3 { 23 | margin: 0px; 24 | } 25 | 26 | .languages { 27 | list-style: none; 28 | padding: 20px 30px; 29 | margin-top: 20px; 30 | border-top: 1px solid #eee; 31 | 32 | .clearfix(); 33 | 34 | li { 35 | width: 50%; 36 | float: left; 37 | padding: 10px 5px; 38 | font-size: 16px; 39 | 40 | a { 41 | 42 | } 43 | 44 | @media (max-width: 600px) { 45 | width: 100%; 46 | max-width: 100%; 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/markdown.less: -------------------------------------------------------------------------------- 1 | .book .book-body .page-wrapper .page-inner section { 2 | display: none; 3 | } 4 | 5 | .book .book-body .page-wrapper .page-inner section.normal { 6 | .markdown-content(@content-color, @content-line-height); 7 | 8 | pre, code { 9 | @import "./highlight/white.less"; 10 | } 11 | 12 | .glossary-term { 13 | cursor: help; 14 | text-decoration: underline; 15 | } 16 | } 17 | 18 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal { 19 | color: @page-color-1; 20 | 21 | a { 22 | color: @page-link-color-1; 23 | } 24 | 25 | h1, 26 | h2, 27 | h3, 28 | h4, 29 | h5, 30 | h6 { 31 | color: @page-heading-color-1; 32 | } 33 | 34 | h1, h2 { 35 | border-color: @page-heading-border-color-1; 36 | } 37 | 38 | h6 { 39 | color: @page-h6-color-1; 40 | } 41 | 42 | hr { 43 | background-color: @page-hr-color-1; 44 | } 45 | 46 | blockquote { 47 | border-color: @page-blockquote-border-color-1; 48 | } 49 | 50 | pre, code { 51 | background: @page-pre-background-1; 52 | color: @page-pre-color-1; 53 | border-color: @page-pre-border-color-1; 54 | 55 | @import "./highlight/sepia.less"; 56 | } 57 | 58 | .highlight { 59 | background-color: @page-highlight-background-1; 60 | } 61 | 62 | table { 63 | th, td { 64 | border-color: @page-table-header-border-color-1; 65 | } 66 | 67 | tr { 68 | color: @page-table-row-color-1; 69 | background-color: @page-table-row-background-1; 70 | border-color: @page-table-row-border-color-1; 71 | } 72 | 73 | tr:nth-child(2n) { 74 | background-color: @page-table-row-odd-background-1; 75 | } 76 | } 77 | } 78 | 79 | .book.color-theme-2 .book-body .page-wrapper .page-inner section.normal { 80 | color: @page-color-2; 81 | 82 | a { 83 | color: @page-link-color-2; 84 | } 85 | 86 | h1, 87 | h2, 88 | h3, 89 | h4, 90 | h5, 91 | h6 { 92 | color: @page-heading-color-2; 93 | } 94 | 95 | h1, h2 { 96 | border-color: @page-heading-border-color-2; 97 | } 98 | 99 | h6 { 100 | color: @page-h6-color-2; 101 | } 102 | 103 | hr { 104 | background-color: @page-hr-color-2; 105 | } 106 | 107 | blockquote { 108 | border-color: @page-blockquote-border-color-2; 109 | } 110 | 111 | pre, code { 112 | color: @page-pre-color-2; 113 | background: @page-pre-background-2; 114 | border-color: @page-pre-border-color-2; 115 | 116 | @import "./highlight/night.less"; 117 | } 118 | 119 | .highlight { 120 | background-color: @page-highlight-background-2; 121 | } 122 | 123 | table { 124 | th, td { 125 | border-color: @page-table-header-border-color-2; 126 | } 127 | 128 | tr { 129 | color: @page-table-row-color-2; 130 | background-color: @page-table-row-background-2; 131 | border-color: @page-table-row-border-color-2; 132 | } 133 | 134 | tr:nth-child(2n) { 135 | background-color: @page-table-row-odd-background-2; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /theme/stylesheets/website/navigation.less: -------------------------------------------------------------------------------- 1 | .book .book-body { 2 | .navigation { 3 | position: absolute; 4 | top: @header-height; 5 | bottom: 0px; 6 | margin: 0; 7 | max-width: 150px; 8 | min-width: 90px; 9 | 10 | display: flex; 11 | justify-content: center; 12 | align-content: center; 13 | flex-direction: column; 14 | 15 | font-size: 40px; 16 | color: @navigation-color; 17 | 18 | text-align: center; 19 | 20 | .transition(all 350ms ease); 21 | 22 | &:hover { 23 | text-decoration: none; 24 | color: @navigation-hover-color; 25 | } 26 | 27 | &.navigation-next { 28 | right: 0px; 29 | } 30 | &.navigation-prev { 31 | left: 0px; 32 | } 33 | } 34 | 35 | @media (max-width: @mobileMaxWidth) { 36 | .navigation { 37 | position: static; 38 | top: auto; 39 | max-width: 50%; 40 | width: 50%; 41 | display: inline-block; 42 | float: left; 43 | 44 | &.navigation-unique { 45 | max-width: 100%; 46 | width: 100%; 47 | } 48 | } 49 | } 50 | } 51 | .book.color-theme-1 .book-body { 52 | .navigation { 53 | color: @navigation-color-1; 54 | 55 | &:hover { 56 | color: @navigation-hover-color-1; 57 | } 58 | } 59 | } 60 | .book.color-theme-2 .book-body { 61 | .navigation { 62 | color: @navigation-color-2; 63 | 64 | &:hover { 65 | color: @navigation-hover-color-2; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /theme/stylesheets/website/search-highlight.less: -------------------------------------------------------------------------------- 1 | .book .book-body { 2 | .book-search-highlight { 3 | background-color: #FFFF04; 4 | } 5 | } 6 | .book.color-theme-1 .book-body { 7 | .book-search-highlight { 8 | background-color: #FFFF04; 9 | } 10 | } 11 | .book.color-theme-2 .book-body { 12 | .book-search-highlight { 13 | background-color: #FFFF04; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /theme/templates/ebook/glossary.html: -------------------------------------------------------------------------------- 1 | {% extends "./page.html" %} 2 | 3 | {% block title %}Glossary | {{ title }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

Glossary

8 | {% for item in glossaryIndex %} 9 |
10 |

{{ item.name }}

11 |

{{ item.description }}

12 |
13 | {% endfor %} 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /theme/templates/ebook/includes/exercise.html: -------------------------------------------------------------------------------- 1 |
Exercise
2 |
3 | {% autoescape false %}{{ section.content }}{% endautoescape %} 4 |
5 |
6 |
{% autoescape false %}{{ section.code.base|code }}{% endautoescape %}
7 |
8 |
9 |
10 |
{% autoescape false %}{{ section.code.solution|code }}{% endautoescape %}
11 |
12 | -------------------------------------------------------------------------------- /theme/templates/ebook/includes/quiz.html: -------------------------------------------------------------------------------- 1 |
Quiz
2 |
{% autoescape false %}{{ section.content }}{% endautoescape %}
3 | {% for quiz in section.quiz %} 4 |
5 |
Question {{ loop.index }} of {{ section.quiz.length }}
6 | 7 |
8 | {% autoescape false %}{{ quiz.base }}{% endautoescape %} 9 |
10 |
11 | {% autoescape false %}{{ quiz.solution }}{% endautoescape %} 12 |
13 |
14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /theme/templates/ebook/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ htmlSnippet("html:start")|default("") }} 4 | 5 | {{ htmlSnippet("head:start")|default("") }} 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | {% block head %}{% endblock %} 12 | {{ htmlSnippet("head:end")|default("") }} 13 | 14 | 15 | {{ htmlSnippet("body:start")|default("") }} 16 | {% block style %}{% endblock %} 17 | {% block content %}{% endblock %} 18 | {% block javascript %}{% endblock %} 19 | {{ htmlSnippet("body:end")|default("") }} 20 | 21 | {{ htmlSnippet("html:end")|default("") }} 22 | 23 | -------------------------------------------------------------------------------- /theme/templates/ebook/page.html: -------------------------------------------------------------------------------- 1 | {% extends "./layout.html" %} 2 | 3 | {% block title %}{{ progress.current.title }} | {{ title }}{% endblock %} 4 | 5 | {% block style %} 6 | 7 | {% for resource in plugins.resources.css %} 8 | {% if resource.url %} 9 | 10 | {% else %} 11 | 12 | {% endif %} 13 | {% endfor %} 14 | {% for style in styles %} 15 | 16 | {% endfor %} 17 | {% endblock %} 18 | 19 | {% block content %} 20 |
21 |

{{ progress.current.title }}

22 | {% for section in content %} 23 |
24 | {% if section.type == "normal" %} 25 | {% autoescape false %}{{ section.content }}{% endautoescape %} 26 | {% elif section.type == "exercise" %} 27 | {% include "./includes/exercise.html" with {section: section} %} 28 | {% elif section.type == "quiz" %} 29 | {% include "./includes/quiz.html" with {section: section} %} 30 | {% endif %} 31 |
32 | {% endfor %} 33 |
34 | {% endblock %} 35 | 36 | -------------------------------------------------------------------------------- /theme/templates/ebook/summary.html: -------------------------------------------------------------------------------- 1 | {% extends "./page.html" %} 2 | 3 | {% block title %}Table of Contents | {{ title }}{% endblock %} 4 | 5 | {% macro articles(_articles) %} 6 | {% for item in _articles %} 7 | {% set externalLink = item.path|isExternalLink %} 8 |
  • 9 | {% if item.path %} 10 | {% if !externalLink %} 11 | {{ item.title }} 12 | {% else %} 13 | {{ item.title }} 14 | {% endif %} 15 | {% else %} 16 | {{ item.title }} 17 | {% endif %} 18 | {% if item.articles.length > 0 %} 19 |
      20 | {{ articles(item.articles) }} 21 |
    22 | {% endif %} 23 |
  • 24 | {% endfor %} 25 | {% endmacro %} 26 | 27 | {% block content %} 28 |
    29 |

    Table of Contents

    30 |
      31 | {{ articles(summary.chapters) }} 32 | 33 | {% if glossary.length > 0 %} 34 |
    1. Glossary
    2. 35 | {% endif %} 36 |
    37 |
    38 | {% endblock %} 39 | 40 | -------------------------------------------------------------------------------- /theme/templates/website/glossary.html: -------------------------------------------------------------------------------- 1 | {% extends "page.html" %} 2 | 3 | {% block title %}Glossary | {{ title }}{% endblock %} 4 | 5 | {% block page_inner %} 6 | {% for item in glossaryIndex %} 7 |
    8 |

    {{ item.name }}

    9 |

    {{ item.description }}

    10 |

    Index

    11 | 16 |
    17 | {% endfor %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /theme/templates/website/includes/exercise.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Exercise

    3 |
    4 |
    5 | Correct! 6 |
    7 | 8 |
    9 | False! 10 |
    11 | 12 |
    13 | {% autoescape false %}{{ section.content }}{% endautoescape %} 14 |
    15 |
    {{ section.code.base }}
    16 | 17 | 18 | 19 | {% if section.code.context %} 20 | 21 | {% endif %} 22 | 23 |
    24 | Submit 25 | Solution 26 |
    27 | -------------------------------------------------------------------------------- /theme/templates/website/includes/font-settings.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /theme/templates/website/includes/header.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | {% if glossary.length > 0 %} 6 | 7 | {% endif %} 8 | 13 | 14 | 15 | {% if options.links.sharing.all !== false %} 16 | 29 | {% endif %} 30 | 31 | {% if options.links.sharing.google === true %} 32 | 33 | {% endif %} 34 | {% if options.links.sharing.facebook === true %} 35 | 36 | {% endif %} 37 | {% if options.links.sharing.twitter === true %} 38 | 39 | {% endif %} 40 | {% if options.links.sharing.weibo === true %} 41 | 42 | {% endif %} 43 | {% if options.links.sharing.qq === true %} 44 | 45 | {% endif %} 46 | {% if options.links.sharing.qrcode === true %} 47 | 59 | {% endif %} 60 | 61 | 62 |

    63 | 64 | {{ title }} 65 |

    66 |
    67 | -------------------------------------------------------------------------------- /theme/templates/website/includes/quiz.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Quiz

    3 |
    4 | 5 |
    6 | {% autoescape false %}{{ section.content }}{% endautoescape %} 7 |
    8 | 9 | 10 | {% for quiz in section.quiz %} 11 |
    12 |
    Question {{ loop.index }} of {{ section.quiz.length }}
    13 | 14 |
    15 | 18 | 19 | 23 | 24 |
    25 | {% autoescape false %}{{ quiz.base }}{% endautoescape %} 26 |
    27 |
    28 | 29 | 34 |
    35 | {% endfor %} 36 | 37 |
    38 | Submit 39 | Solution 40 |
    41 | -------------------------------------------------------------------------------- /theme/templates/website/includes/summary.html: -------------------------------------------------------------------------------- 1 | {% macro articles(_articles) %} 2 | {% for item in _articles %} 3 | {% set externalLink = item.path|isExternalLink %} 4 |
  • 5 | {% if item.path %} 6 | {% if !externalLink %} 7 | 8 | 9 | {% if item.level !== "0" %} 10 | {{ item.level }}. 11 | {% endif %} 12 | {{ item.title }} 13 | 14 | {% else %} 15 | 16 | 17 | {% if item.level !== "0" %} 18 | {{ item.level }}. 19 | {% endif %} 20 | {{ item.title }} 21 | 22 | {% endif %} 23 | {% else %} 24 | {{ item.level }}. {{ item.title }} 25 | {% endif %} 26 | {% if item.articles.length > 0 %} 27 |
      28 | {{ articles(item.articles) }} 29 |
    30 | {% endif %} 31 |
  • 32 | {% endfor %} 33 | {% endmacro %} 34 | 35 |
    36 | 39 |
      40 | {% set _divider = false %} 41 | {% if options.links.sidebar %} 42 | {% for link in options.links.sidebar %} 43 | {% set _divider = true %} 44 |
    • 45 | {{ loop.key }} 46 |
    • 47 | {% endfor %} 48 | {% endif %} 49 | 50 | {% if _divider %} 51 |
    • 52 | {% endif %} 53 | 54 | {{ articles(summary.chapters) }} 55 | 56 | {% if options.links.tail !== false %} 57 |
    • 58 | {% for link in options.links.tail %} 59 | {% set _divider = true %} 60 |
    • 61 | {{ loop.key }} 62 |
    • 63 | {% endfor %} 64 | {% endif %} 65 |
    66 |
    67 | -------------------------------------------------------------------------------- /theme/templates/website/langs.html: -------------------------------------------------------------------------------- 1 | {% extends "./layout.html" %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | 5 | {% block style %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 |
    12 |

    Choose a language

    13 | 14 |
      15 | {% for lang in langs %} 16 |
    • 17 | {{ lang.title }} 18 |
    • 19 | {% endfor %} 20 |
    21 |
    22 |
    23 | {% endblock %} 24 | 25 | {% block javascript %}{% endblock %} -------------------------------------------------------------------------------- /theme/templates/website/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ htmlSnippet("html:start")|default("") }} 4 | 5 | {{ htmlSnippet("head:start")|default("") }} 6 | 7 | 8 | {% block title %}{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% if options.isbn %} 20 | 21 | {% endif %} 22 | {% block head %}{% endblock %} 23 | {{ htmlSnippet("head:end")|default("") }} 24 | 25 | 26 | {{ htmlSnippet("body:start")|default("") }} 27 | {% block style %}{% endblock %} 28 | {% block content %}{% endblock %} 29 | {% block javascript %}{% endblock %} 30 | {{ htmlSnippet("body:end")|default("") }} 31 | 32 | {{ htmlSnippet("html:end")|default("") }} 33 | 34 | -------------------------------------------------------------------------------- /theme/templates/website/page.html: -------------------------------------------------------------------------------- 1 | {% extends "./layout.html" %} 2 | 3 | {% block head %} 4 | {% parent %} 5 | {% if progress.current.next and progress.current.next.path %} 6 | 7 | {% endif %} 8 | {% if progress.current.prev and progress.current.prev.path %} 9 | 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block title %}{{ progress.current.title }} | {{ title }}{% endblock %} 14 | {% block description %}{{ description }}{% endblock %} 15 | {% block keywords %}{{ keywords }}{% endblock %} 16 | 17 | {% block content %} 18 |
    19 | {% include "includes/summary.html" %} 20 |
    21 |
    22 | {% include "includes/header.html" %} 23 |
    24 |
    25 | {% block page_inner %} 26 | {% for section in content %} 27 |
    28 | {% if section.type == "normal" %} 29 | {% autoescape false %}{{ section.content }}{% endautoescape %} 30 | {% elif section.type == "exercise" %} 31 | {% include "./includes/exercise.html" with {section: section} %} 32 | {% elif section.type == "quiz" %} 33 | {% include "./includes/quiz.html" with {section: section} %} 34 | {% endif %} 35 |
    36 | {% endfor %} 37 | {% endblock %} 38 |
    39 |
    40 |
    41 | 42 | {% if progress.current.prev and progress.current.prev.path %} 43 | 44 | {% endif %} 45 | {% if progress.current.next and progress.current.next.path %} 46 | 47 | {% endif %} 48 |
    49 |
    50 | {% endblock %} 51 | 52 | {% block javascript %} 53 | 54 | {% for resource in plugins.resources.js %} 55 | {% if resource.url %} 56 | 57 | {% else %} 58 | 59 | {% endif %} 60 | {% endfor %} 61 | 67 | {% endblock %} 68 | 69 | {% block style %} 70 | 71 | {% for resource in plugins.resources.css %} 72 | {% if resource.url %} 73 | 74 | {% else %} 75 | 76 | {% endif %} 77 | {% endfor %} 78 | {% for style in styles %} 79 | 80 | {% endfor %} 81 | {% endblock %} 82 | --------------------------------------------------------------------------------