Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cc23b7e34 | |||
| 61b6848a95 | |||
| 9265b0185a | |||
| 392573a990 | |||
| 554d6a6170 | |||
| 2182baa2e9 | |||
| 7fe189907e | |||
| 9d541a0a4d | |||
| 8f3df085d8 | |||
| b7e04b5475 | |||
| ee8bf6dc4c | |||
| 9670f564d9 | |||
| b541df2b93 | |||
| cf3a641350 | |||
| ec719891a1 | |||
| 1bfa8c186c | |||
| 5887f1de28 | |||
| a9a66bc240 | |||
| 742229a989 | |||
| 9fe80b878e | |||
| 4b64c87011 | |||
| 4c36c592c1 | |||
| 9ffb1a062f | |||
| 536c480969 | |||
| d14bd7245f | |||
| 6fc8255b7f | |||
| a42f287a92 | |||
| d2f62fd109 | |||
| f24b66f2c4 | |||
| 607b45f172 | |||
| c166a9863e | |||
| 1a4b196851 | |||
| ff52a2317b | |||
| b598c9a272 | |||
| 9cd7c9ad0e | |||
| 75a8d3069d | |||
| ed41429d58 | |||
| eecb33baa8 | |||
| b8f2da734e | |||
| 3687a1701b | |||
| c8f83e2684 | |||
| aa7835e913 | |||
| 74edc4f279 | |||
| 696cbdc3b1 | |||
| 2f8543d710 | |||
| aee33d67b6 | |||
| 52ccc2b46d | |||
| 9270d7949a | |||
| 855c9b9aef | |||
| c98ba22b08 | |||
| d1e55aef8e | |||
| ef059c8737 | |||
| fde66ed849 | |||
| abce29866e | |||
| 014b2f9c69 | |||
| 65cb23feab | |||
| 2b3c67a26b | |||
| 174ce965d9 | |||
| adf20e6a16 | |||
| 53f714b7f3 | |||
| d19906d5b2 | |||
| fb26b634ce | |||
| a34dc458c9 | |||
| eeb715d5d6 | |||
| a8e026a642 | |||
| cc096d2253 | |||
| a4c1cf9334 | |||
| 9ffec175a5 | |||
| 465188cae6 | |||
| 4547ec73fd | |||
| cae6267665 | |||
| 58bee15f37 | |||
| 761cfb4a2c | |||
| 8e3339ead2 | |||
| b330277ed7 | |||
| 12b5baf433 | |||
| f38d68c355 | |||
| 865bf40e1a | |||
| 562ee2b77f | |||
| 8b0f2f6329 | |||
| 44717a3e3e | |||
| 6f7e3fda02 | |||
| 511fc7bd46 | |||
| 4987beec2b | |||
| e414cbd6d1 | |||
| 694afeccc5 | |||
| 959a2d830a | |||
| 4850c0f302 | |||
| ac258329ff | |||
| d5bd08dcd2 | |||
| 6c414ec400 | |||
| 881a0c6f6d | |||
| 0071594300 | |||
| 460799828f | |||
| 02b75288d2 | |||
| b108be0c3b | |||
| 7ab285832b | |||
| 0bc288fe96 | |||
| 9fbe240843 | |||
| 0c7e2b3557 | |||
| e34e65813c | |||
| 63f946ced9 | |||
| af9f3cf89f | |||
| 355094bbb3 | |||
| b3adedb468 | |||
| 026e4362d8 | |||
| 9cf5384fbe | |||
| 9bc0bf19da | |||
| f49a1a484e | |||
| 446d486a0f | |||
| 07101deaef | |||
| 76b3fcb41c | |||
| febda9aba1 | |||
| f952fa47b9 | |||
| 1984660930 | |||
| d475affd3b | |||
| fe3de93e06 | |||
| 5cae311edf | |||
| 26701a2a73 | |||
| 5125e04814 | |||
| 0a6194340f | |||
| e0d96b3163 | |||
| f656390783 | |||
| 88c480ba4f | |||
| 3da53d56a2 | |||
| 91359fab1e | |||
| a52a4bed1a | |||
| 9dce855113 | |||
| 57e2595a3e | |||
| b1314273bb | |||
| cb9a0b82ba | |||
| 57ecfe4a54 | |||
| 405792fd8f | |||
| fc6d911340 | |||
| 1e6307b69b |
@@ -0,0 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
- "0.8"
|
||||
@@ -16,7 +16,8 @@ module.exports = function (grunt) {
|
||||
optimization: 2
|
||||
},
|
||||
files: {
|
||||
"assets/static/style.css": "assets/stylesheets/main.less"
|
||||
"theme/assets/style.css": "theme/stylesheets/main.less",
|
||||
"theme/assets/print.css": "theme/stylesheets/print.less"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -24,16 +25,18 @@ module.exports = function (grunt) {
|
||||
compile: {
|
||||
options: {
|
||||
name: "app",
|
||||
baseUrl: "assets/javascript/",
|
||||
out: "assets/static/app.js",
|
||||
baseUrl: "theme/javascript/",
|
||||
out: "theme/assets/app.js",
|
||||
preserveLicenseComments: false,
|
||||
optimize: "uglify",
|
||||
optimize: "uglify", //"uglify",
|
||||
include: ["requireLib"],
|
||||
paths: {
|
||||
"jQuery": 'vendors/jquery',
|
||||
"lodash": 'vendors/lodash',
|
||||
"requireLib": 'vendors/require',
|
||||
"Mousetrap": 'vendors/mousetrap'
|
||||
"Mousetrap": 'vendors/mousetrap',
|
||||
"mixpanel": 'vendors/mixpanel',
|
||||
"lunr": path.join(__dirname, "node_modules/lunr/lunr")
|
||||
},
|
||||
shim: {
|
||||
'jQuery': {
|
||||
@@ -44,6 +47,12 @@ module.exports = function (grunt) {
|
||||
},
|
||||
'Mousetrap': {
|
||||
exports: 'Mousetrap'
|
||||
},
|
||||
'mixpanel': {
|
||||
exports: 'mixpanel'
|
||||
},
|
||||
'lunr': {
|
||||
exports: 'lunr'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
GitBook
|
||||
=======
|
||||
|
||||
GiBook is a command line tool (and Node.js library) for building beautiful programming books and exercises using GitHub/Git and Markdown. You can see an example: [Learn Javascript](http://gitbookio.github.io/javascript/).
|
||||
[](https://travis-ci.org/GitbookIO/gitbook)
|
||||
|
||||
GitBook is a command line tool (and Node.js library) for building beautiful programming books and exercises using GitHub/Git and Markdown. You can see an example: [Learn Javascript](http://gitbookio.github.io/javascript/).
|
||||
|
||||

|
||||
|
||||
@@ -31,8 +33,23 @@ Options for commands `build` and `serve` are:
|
||||
-t, --title <name> Name of the book to generate, defaults to repo name
|
||||
-i, --intro <intro> Description of the book to generate
|
||||
-g, --github <repo_path> ID of github repo like : username/repo
|
||||
-o, --output <directory> Path to output directory, defaults to ./_book
|
||||
-f, --format <name> Change generation format, defaults to site, availables are: site, page, pdf, json
|
||||
-i, --intro <intro> Description of the book to generate
|
||||
--githubHost <url> The url of the github host (defaults to https://github.com/)
|
||||
--theme <path> Path to theme directory
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
GitBook can generate your book in the following formats:
|
||||
|
||||
* **Static Website**: This is the default format, it generates a complete interactive static website that can be for example hosted on GitHub Pages.
|
||||
* **PDF**: A complete PDF book with exercise solutions at the end of the book. Generate to this format using: ```gitbook pdf ./myrepo```, you need to have [gitbook-pdf](https://github.com/GitbookIO/gitbook-pdf) installed.
|
||||
* **eBook**: A complete eBook with exercise solutions at the end of the book. Generate to this format using: ```gitbook ebook ./myrepo```, you need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed.
|
||||
* **Single Page**: The book will be stored in a single printable HTML page, this format is used for conversion to PDF or eBook. Generate to this format using: ```gitbook build ./myrepo -f page```.
|
||||
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate to this format using: ```gitbook build ./myrepo -f json```.
|
||||
|
||||
## Book Format
|
||||
|
||||
A book is a GitHub repository containing at least 2 files: `README.md` and `SUMMARY.md`.
|
||||
@@ -72,22 +89,46 @@ An exercise is defined by 4 simple parts:
|
||||
* **Solution** code, being a correct solution to the exercise
|
||||
* **Validation** code that tests the correctness of the user's input
|
||||
|
||||
Exercises need to start and finish with a separation bar (```---``` or ```***```). It should contain 3 code elements (**base**, **solution** and **validation**).
|
||||
Exercises need to start and finish with a separation bar (```---``` or ```***```). It should contain 3 code elements (**base**, **solution** and **validation**). It can contain a 4th element that provides **context** code (functions, imports of libraries etc ... that shouldn't be displayed to the user).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Define a variable `x` equal to 10.
|
||||
|
||||
|
||||
```js
|
||||
var x =
|
||||
var x =
|
||||
```
|
||||
|
||||
|
||||
```js
|
||||
var x = 10;
|
||||
```
|
||||
|
||||
|
||||
```js
|
||||
assert(x == 10);
|
||||
```
|
||||
|
||||
|
||||
```js
|
||||
// This is context code available everywhere
|
||||
// The user will be able to call magicFunc in his code
|
||||
function magicFunc() {
|
||||
return 3;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Multi-Languages
|
||||
|
||||
GitBook supports building books written in multiple languages. Each language should be a sub-directory following the normal GitBook format, and a file named `LANGS.md` should be present at the root of the repository with the following format:
|
||||
|
||||
```
|
||||
* [English](en/)
|
||||
* [French](fr/)
|
||||
* [Español](es/)
|
||||
```
|
||||
|
||||
You can see a complete example with the [Learn Git](https://github.com/GitbookIO/git) book.
|
||||
|
||||
#### Ignoring files & folders
|
||||
|
||||
GitBook will read the `.gitignore`, `.bookignore` and `.ignore` files to get a list of files and folders to skip. (The format inside those files, follows the same convention as `.gitignore`)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
define([
|
||||
"utils/storage",
|
||||
"utils/platform",
|
||||
"core/state"
|
||||
], function(storage, platform, state) {
|
||||
|
||||
// Toggle sidebar with or withour animation
|
||||
var toggleSidebar = function(_state, animation) {
|
||||
if (-state != null && isOpen() == _state) return;
|
||||
if (animation == null) animation = true;
|
||||
|
||||
var $book = state().$book;
|
||||
$book.toggleClass("without-animation", !animation);
|
||||
$book.toggleClass("with-summary", _state);
|
||||
|
||||
storage.set("sidebar", isOpen());
|
||||
};
|
||||
|
||||
// Return true if sidebar is open
|
||||
var isOpen = function() {
|
||||
return state().$book.hasClass("with-summary");
|
||||
};
|
||||
|
||||
// Prepare sidebar: state and toggle button
|
||||
var init = function() {
|
||||
var $book = state().$book;
|
||||
|
||||
// Toggle summary
|
||||
$book.find(".book-header .toggle-summary").click(function(e) {
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
});
|
||||
|
||||
// Init last state if not mobile and not homepage
|
||||
toggleSidebar(platform.isMobile ? false : storage.get("sidebar", true), false);
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
toggle: toggleSidebar
|
||||
}
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
define([
|
||||
"jQuery"
|
||||
], function() {
|
||||
return function() {
|
||||
var $book = $(".book");
|
||||
|
||||
return {
|
||||
'$book': $book,
|
||||
|
||||
'githubId': $book.data("github"),
|
||||
'level': $book.data("level")
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
define([], function() {
|
||||
var isAvailable = function() {
|
||||
return (typeof mixpanel !== "undefined");
|
||||
};
|
||||
|
||||
var track = function(event, data) {
|
||||
if (!isAvailable()) {
|
||||
console.log("tracking not available!");
|
||||
return;
|
||||
}
|
||||
mixpanel.track(event, data);
|
||||
};
|
||||
|
||||
return {
|
||||
isAvailable: isAvailable,
|
||||
track: track
|
||||
};
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
.book {
|
||||
.page-footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
color: #fff;
|
||||
|
||||
.navigation-link {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
line-height: 60px;
|
||||
border-radius: 2px;
|
||||
|
||||
background: @brand-success;
|
||||
color: inherit;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
background: darken(@btn-success-bg, 5%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.coming-soon, &.coming-soon:hover {
|
||||
background: #f0ad4e;
|
||||
}
|
||||
&.finished, &.finished:hover {
|
||||
background: darken(@btn-success-bg, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
var path = require('path');
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
var tmp = require('tmp');
|
||||
|
||||
var utils = require('./utils');
|
||||
|
||||
var generate = require("../lib/generate");
|
||||
var parse = require("../lib/parse");
|
||||
var fs = require('../lib/generate/fs');
|
||||
var generators = require("../lib/generate").generators;
|
||||
|
||||
var buildFunc = function(dir, options) {
|
||||
dir = dir || process.cwd();
|
||||
outputDir = options.output || path.join(dir, '_book');
|
||||
|
||||
console.log('Starting build ...');
|
||||
// Get repo's URL
|
||||
return utils.gitURL(dir)
|
||||
.then(function(url) {
|
||||
// Get ID of repo
|
||||
return utils.githubID(url);
|
||||
}, function(err) {
|
||||
return null;
|
||||
})
|
||||
.then(function(repoID) {
|
||||
var githubID = options.github || repoID;
|
||||
|
||||
if(!githubID) {
|
||||
throw new Error('Needs a githubID (username/repo). Either set repo origin to a github repo or use the -g flag');
|
||||
}
|
||||
|
||||
var parts = githubID.split('/', 2);
|
||||
var user = parts[0], repo = parts[1];
|
||||
|
||||
var title = options.title || utils.titleCase(repo);
|
||||
|
||||
return generate.folder(
|
||||
_.extend(options.options || {}, {
|
||||
input: dir,
|
||||
output: outputDir,
|
||||
title: title,
|
||||
description: options.intro,
|
||||
github: githubID,
|
||||
githubHost: options.githubHost,
|
||||
generator: options.format,
|
||||
theme: options.theme
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function(output) {
|
||||
console.log("Successfuly built !");
|
||||
return output;
|
||||
}, utils.logError);
|
||||
};
|
||||
|
||||
var buildFiles = function(dir, outputFile, options, masterOptions) {
|
||||
var ext = masterOptions.extension;
|
||||
|
||||
outputFile = outputFile || path.resolve(dir, "book."+ext);
|
||||
|
||||
Q.nfcall(tmp.dir)
|
||||
.then(function(tmpDir) {
|
||||
return buildFunc(
|
||||
dir,
|
||||
_.extend(options, {
|
||||
output: tmpDir,
|
||||
format: masterOptions.format,
|
||||
options: masterOptions.options
|
||||
})
|
||||
)
|
||||
.then(function(_options) {
|
||||
var copyPDF = function(lang) {
|
||||
var _outputFile = outputFile;
|
||||
var _tmpDir = tmpDir;
|
||||
|
||||
if (lang) {
|
||||
_outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+"_"+lang+path.extname(_outputFile);
|
||||
_tmpDir = path.join(_tmpDir, lang);
|
||||
}
|
||||
|
||||
console.log("Generating in", _outputFile);
|
||||
return fs.copy(
|
||||
path.join(_tmpDir, "index."+ext),
|
||||
_outputFile
|
||||
);
|
||||
};
|
||||
|
||||
// Multi-langs book
|
||||
return Q()
|
||||
.then(function() {
|
||||
if (_options.langsSummary) {
|
||||
console.log("Generating for all the languages");
|
||||
return Q.all(
|
||||
_.map(_options.langsSummary.list, function(lang) {
|
||||
return copyPDF(lang.lang);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return copyPDF();
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
return fs.remove(tmpDir);
|
||||
})
|
||||
.fail(utils.logError);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
folder: buildFunc,
|
||||
files: buildFiles
|
||||
};
|
||||
@@ -1,110 +1,86 @@
|
||||
#! /usr/bin/env node
|
||||
|
||||
// Requires
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var prog = require('commander');
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
var pkg = require('../package.json');
|
||||
var generate = require("../lib/generate");
|
||||
var parse = require("../lib/parse");
|
||||
var generators = require("../lib/generate").generators;
|
||||
var fs = require('../lib/generate/fs');
|
||||
|
||||
var utils = require('./utils');
|
||||
|
||||
var build = require('./build');
|
||||
|
||||
// General options
|
||||
prog
|
||||
.version(pkg.version)
|
||||
.option('-d, --dir <source_directory>', 'Source directory of book, containing Markdown files');
|
||||
.version(pkg.version);
|
||||
|
||||
var buildCommand = function(command, action) {
|
||||
return command
|
||||
.option('-o, --output <directory>', 'Path to output directory, defaults to ./_book')
|
||||
.option('-f, --format <name>', 'Change generation format, defaults to site, availables are: '+_.keys(generators).join(", "))
|
||||
.option('-t, --title <name>', 'Name of the book to generate, defaults to repo name')
|
||||
.option('-i, --intro <intro>', 'Description of the book to generate')
|
||||
.option('-g, --github <repo_path>', 'ID of github repo like : username/repo')
|
||||
.option('--githubHost <url>', 'The url of the github host (defaults to https://github.com/')
|
||||
.option('--theme <path>', 'Path to theme directory')
|
||||
.action(action);
|
||||
}
|
||||
|
||||
var buildFunc;
|
||||
prog
|
||||
buildCommand(prog
|
||||
.command('build [source_dir]')
|
||||
.description('Build a gitbook from a directory')
|
||||
.option('-o, --output <directory>', 'Path to output directory, defaults to ./_book')
|
||||
.option('-t, --title <name>', 'Name of the book to generate, defaults to repo name')
|
||||
.option('-i, --intro <intro>', 'Description of the book to generate')
|
||||
.option('-g, --github <repo_path>', 'ID of github repo like : username/repo')
|
||||
.action(buildFunc = function(dir, options) {
|
||||
dir = dir || process.cwd();
|
||||
outputDir = options.output || path.join(dir, '_book');
|
||||
.description('Build a gitbook from a directory'), build.folder);
|
||||
|
||||
console.log('Starting build ...');
|
||||
// Get repo's URL
|
||||
return utils.gitURL(dir)
|
||||
.then(function(url) {
|
||||
// Get ID of repo
|
||||
return utils.githubID(url);
|
||||
}, function(err) {
|
||||
return null;
|
||||
})
|
||||
.then(function(repoID) {
|
||||
var githubID = options.github || repoID;
|
||||
|
||||
if(!githubID) {
|
||||
throw new Error('Needs a githubID (username/repo). Either set repo origin to a github repo or use the -g flag');
|
||||
}
|
||||
|
||||
var parts = githubID.split('/', 2);
|
||||
var user = parts[0], repo = parts[1];
|
||||
|
||||
var title = options.title || utils.titleCase(repo);
|
||||
|
||||
return generate.folder(
|
||||
dir,
|
||||
outputDir,
|
||||
{
|
||||
title: title,
|
||||
description: options.intro,
|
||||
github: githubID
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function(output) {
|
||||
console.log("Successfuly built !");
|
||||
}, function(err) {
|
||||
throw err;
|
||||
})
|
||||
.then(_.constant(outputDir));
|
||||
});
|
||||
|
||||
prog
|
||||
buildCommand(prog
|
||||
.command('serve [source_dir]')
|
||||
.description('Build then serve a gitbook from a directory')
|
||||
.option('-p, --port <port>', 'Port for server to listen on', 4000)
|
||||
.option('-o, --output <directory>', 'Path to output directory, defaults to ./_book')
|
||||
.option('-t, --title <name>', 'Name of the book to generate, defaults to repo name')
|
||||
.option('-g, --github <repo_path>', 'ID of github repo like : username/repo')
|
||||
.action(function(dir, options) {
|
||||
buildFunc(dir, options)
|
||||
.then(function(outputDir) {
|
||||
.option('-p, --port <port>', 'Port for server to listen on', 4000),
|
||||
function(dir, options) {
|
||||
build.folder(dir, options)
|
||||
.then(function(_options) {
|
||||
console.log();
|
||||
console.log('Starting server ...');
|
||||
return utils.serveDir(outputDir, options.port);
|
||||
return utils.serveDir(_options.output, options.port)
|
||||
.fail(utils.logError);
|
||||
})
|
||||
.then(function() {
|
||||
console.log('Serving book on http://localhost:'+options.port);
|
||||
console.log();
|
||||
console.log('Press CTRL+C to quit ...');
|
||||
})
|
||||
.fail(function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
prog
|
||||
.command('lex <page_file>')
|
||||
.description('Parse a page file into sections, display JSON dump')
|
||||
.action(function(page_file) {
|
||||
var parsed = parse.page(fs.readFileSync(page_file, 'utf-8'));
|
||||
|
||||
console.log(JSON.stringify(parsed, null, 4));
|
||||
buildCommand(prog
|
||||
.command('pdf [source_dir] [output_file]')
|
||||
.description('Build a gitbook as a PDF')
|
||||
.option('-pf, --paperformat <format>', 'PDF paper format (default is A4): "5in*7.5in", "10cm*20cm", "A4", "Letter"'),
|
||||
function(dir, output, options) {
|
||||
build.files(dir, output, options, {
|
||||
extension: "pdf",
|
||||
format: "pdf",
|
||||
options: {
|
||||
paperformat: options.paperformat
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
buildCommand(prog
|
||||
.command('ebook [source_dir] [output_file]')
|
||||
.description('Build a gitbook as a eBook')
|
||||
.option('-c, --cover <path>', 'Cover image, default is cover.png if exists'),
|
||||
function(dir, output, options) {
|
||||
var ext = output ? path.extname(output) : "epub";
|
||||
|
||||
build.files(dir, output, options, {
|
||||
extension: ext,
|
||||
format: "ebook",
|
||||
options: {
|
||||
extension: ext,
|
||||
cover: options.cover
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parse and fallback to help if no args
|
||||
if(_.isEmpty(prog.parse(process.argv).args) && process.argv.length === 2) {
|
||||
|
||||
@@ -82,6 +82,11 @@ function serveDir(dir, port) {
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function logError(err) {
|
||||
console.log(err.stack || err.message || err);
|
||||
return Q.reject(err);
|
||||
};
|
||||
|
||||
|
||||
// Exports
|
||||
module.exports = {
|
||||
@@ -89,4 +94,5 @@ module.exports = {
|
||||
githubID: githubID,
|
||||
titleCase: titleCase,
|
||||
serveDir: serveDir,
|
||||
logError: logError
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var fs = require("fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../page");
|
||||
|
||||
/*
|
||||
* This generator inherits from the single page generator
|
||||
* and convert the page output to ebook
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Options for eBook generation
|
||||
this.options = _.defaults(this.options, {
|
||||
extension: "epub"
|
||||
});
|
||||
|
||||
if (!this.options.cover && fs.existsSync(path.join(this.options.input, "cover.png"))) {
|
||||
this.options.cover = path.join(this.options.input, "cover.png")
|
||||
}
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
var that = this;
|
||||
|
||||
return BaseGenerator.prototype.finish.apply(this)
|
||||
.then(function() {
|
||||
var d = Q.defer();
|
||||
|
||||
var _options = {
|
||||
"--cover": that.options.cover
|
||||
};
|
||||
|
||||
var command = [
|
||||
"ebook-convert",
|
||||
path.join(that.options.output, "index.html"),
|
||||
path.join(that.options.output, "index."+that.options.extension),
|
||||
_.chain(_options)
|
||||
.map(function(value, key) {
|
||||
if (value == null) return null;
|
||||
return key+"="+value;
|
||||
})
|
||||
.compact()
|
||||
.value()
|
||||
.join(" ")
|
||||
].join(" ");
|
||||
|
||||
exec(command, function (error, stdout, stderr) {
|
||||
if (error) {
|
||||
if (error.code == 127) {
|
||||
error.message = "Need to install ebook-convert from Calibre";
|
||||
} else {
|
||||
error.message = error.message + " "+stdout;
|
||||
}
|
||||
return d.reject(error);
|
||||
}
|
||||
d.resolve();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -11,7 +11,7 @@ var getFiles = function(path) {
|
||||
|
||||
var ig = Ignore({
|
||||
path: path,
|
||||
ignoreFiles: ['.ignore', '.gitignore']
|
||||
ignoreFiles: ['.ignore', '.gitignore', '.bookignore']
|
||||
});
|
||||
|
||||
// Add extra rules to ignore common folders
|
||||
@@ -27,13 +27,21 @@ var getFiles = function(path) {
|
||||
});
|
||||
|
||||
ig.on('end', function() {
|
||||
d.resolve(files);
|
||||
// Normalize paths on Windows
|
||||
if(process.platform === 'win32') {
|
||||
return d.resolve(files.map(function(file) {
|
||||
return file.replace(/\\/g, '/');
|
||||
}));
|
||||
}
|
||||
|
||||
// Simply return paths otherwise
|
||||
return d.resolve(files);
|
||||
});
|
||||
|
||||
ig.on('error', d.reject);
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
list: getFiles,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var fs = require("./fs");
|
||||
|
||||
var BaseGenerator = function(options) {
|
||||
this.options = options;
|
||||
};
|
||||
|
||||
BaseGenerator.prototype.convertFile = function(content, input) {
|
||||
return Q.reject(new Error("Could not convert "+input));
|
||||
};
|
||||
|
||||
BaseGenerator.prototype.transferFile = function(input) {
|
||||
return fs.copy(
|
||||
path.join(this.options.input, input),
|
||||
path.join(this.options.output, input)
|
||||
);
|
||||
};
|
||||
|
||||
BaseGenerator.prototype.transferFolder = function(input) {
|
||||
return fs.mkdirp(
|
||||
path.join(this.options.output, input)
|
||||
);
|
||||
};
|
||||
|
||||
BaseGenerator.prototype.langsIndex = function(langs) {
|
||||
return Q.reject(new Error("Langs index is not supported in this generator"));
|
||||
};
|
||||
|
||||
BaseGenerator.prototype.finish = function() {
|
||||
return Q.reject(new Error("Could not finish generation"));
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = BaseGenerator;
|
||||
@@ -2,125 +2,147 @@ var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
|
||||
var path = require("path");
|
||||
var swig = require('swig');
|
||||
|
||||
var fs = require("./fs");
|
||||
var parse = require("../parse");
|
||||
var template = require("./template");
|
||||
|
||||
var generate = function(root, output, options) {
|
||||
var files, summary, navigation, tpl;
|
||||
var generators = {
|
||||
"site": require("./site"),
|
||||
"page": require("./page"),
|
||||
"pdf": require("./pdf"),
|
||||
"ebook": require("./ebook"),
|
||||
"json": require("./json")
|
||||
};
|
||||
|
||||
var generate = function(options) {
|
||||
var generator = null;
|
||||
var files;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
// Folders to use
|
||||
input: null,
|
||||
output: null,
|
||||
|
||||
// Output generator
|
||||
generator: "site",
|
||||
|
||||
// Book title, keyword, description
|
||||
title: null,
|
||||
description: "Book generated using GitBook",
|
||||
|
||||
// Origin github repository id
|
||||
github: null
|
||||
github: null,
|
||||
githubHost: 'https://github.com/',
|
||||
|
||||
// Theming
|
||||
theme: path.resolve(__dirname, '../../theme')
|
||||
});
|
||||
|
||||
if (!options.github || !options.title) return Q.reject(new Error("Need options.github and optiosn.title"));
|
||||
if (!options.github || !options.title || !options.input || !options.output) {
|
||||
return Q.reject(new Error("Need options: github, title, input, output"));
|
||||
}
|
||||
|
||||
if (!generators[options.generator]) {
|
||||
return Q.reject(new Error("Invalid generator (availables are: "+_.keys(generators).join(", ")));
|
||||
}
|
||||
|
||||
// Clean output folder
|
||||
return fs.remove(output)
|
||||
return fs.remove(options.output)
|
||||
|
||||
.then(function() {
|
||||
return fs.mkdirp(output);
|
||||
return fs.mkdirp(options.output);
|
||||
})
|
||||
|
||||
// List all files in the repository
|
||||
.then(function() {
|
||||
return fs.list(root);
|
||||
return fs.list(options.input)
|
||||
.then(function(_files) {
|
||||
files = _files;
|
||||
})
|
||||
})
|
||||
|
||||
// Check repository is valid
|
||||
.then(function(_files) {
|
||||
files = _files;
|
||||
// Create the generator
|
||||
.then(function() {
|
||||
generator = new generators[options.generator](options);
|
||||
})
|
||||
|
||||
if (!_.contains(files, "SUMMARY.md") || !_.contains(files, "README.md")) {
|
||||
// Detect multi-languages book
|
||||
.then(function() {
|
||||
if (_.contains(files, "LANGS.md")) {
|
||||
// Multi-languages book
|
||||
return fs.readFile(path.join(options.input, "LANGS.md"), "utf-8")
|
||||
|
||||
// Generate sub-books
|
||||
.then(function(_langsSummary) {
|
||||
options.langsSummary = parse.langs(_langsSummary);
|
||||
|
||||
// Generated a book for each valid entry
|
||||
return Q.all(
|
||||
_.map(options.langsSummary.list, function(entry) {
|
||||
return generate(_.extend({}, options, {
|
||||
input: path.join(options.input, entry.path),
|
||||
output: path.join(options.output, entry.path)
|
||||
}));
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
// Generate languages index
|
||||
.then(function() {
|
||||
return generator.langsIndex(options.langsSummary);
|
||||
});
|
||||
} else if (!_.contains(files, "SUMMARY.md") || !_.contains(files, "README.md")) {
|
||||
// Invalid book
|
||||
return Q.reject(new Error("Invalid gitbook repository, need SUMMARY.md and README.md"));
|
||||
} else {
|
||||
// Generate the book
|
||||
return fs.readFile(path.join(options.input, "SUMMARY.md"), "utf-8")
|
||||
|
||||
// Get summary
|
||||
.then(function(_summary) {
|
||||
options.summary = parse.summary(_summary);
|
||||
|
||||
// Parse navigation
|
||||
options.navigation = parse.navigation(options.summary);
|
||||
})
|
||||
|
||||
// Copy file and replace markdown file
|
||||
.then(function() {
|
||||
return Q.all(
|
||||
_.chain(files)
|
||||
.map(function(file) {
|
||||
if (!file) return;
|
||||
|
||||
if (file[file.length -1] == "/") {
|
||||
return Q(generator.transferFolder(file));
|
||||
} else if (path.extname(file) == ".md" && options.navigation[file] != null) {
|
||||
return fs.readFile(path.join(options.input, file), "utf-8")
|
||||
.then(function(content) {
|
||||
return Q(generator.convertFile(content, file));
|
||||
});
|
||||
} else {
|
||||
return Q(generator.transferFile(file));
|
||||
}
|
||||
})
|
||||
.value()
|
||||
);
|
||||
})
|
||||
|
||||
// Finish gneration
|
||||
.then(function() {
|
||||
return generator.finish();
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Get summary
|
||||
// Return all options
|
||||
.then(function() {
|
||||
return fs.readFile(path.join(root, "SUMMARY.md"), "utf-8")
|
||||
.then(function(_summary) {
|
||||
summary = parse.summary(_summary);
|
||||
|
||||
// Parse navigation
|
||||
navigation = parse.navigation(summary);
|
||||
});
|
||||
})
|
||||
|
||||
// Create template
|
||||
.then(function() {
|
||||
tpl = template({
|
||||
root: root,
|
||||
output: output,
|
||||
locals: {
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
|
||||
githubAuthor: options.github.split("/")[0],
|
||||
githubId: options.github,
|
||||
|
||||
summary: summary,
|
||||
allNavigation: navigation
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Copy file and replace markdown file
|
||||
.then(function() {
|
||||
return Q.all(
|
||||
_.chain(files)
|
||||
.map(function(file) {
|
||||
if (!file) return;
|
||||
var _html = file.replace(".md", ".html");
|
||||
|
||||
// Folder
|
||||
if (file[file.length -1] == "/") {
|
||||
return fs.mkdirp(
|
||||
path.join(output, file)
|
||||
);
|
||||
}
|
||||
|
||||
// Markdown file (only from the summary)
|
||||
else if (path.extname(file) == ".md" && navigation[_html] != null) {
|
||||
return tpl(file, file.replace(".md", ".html"));
|
||||
}
|
||||
|
||||
// Copy file
|
||||
else {
|
||||
return fs.copy(
|
||||
path.join(root, file),
|
||||
path.join(output, file)
|
||||
);
|
||||
}
|
||||
})
|
||||
.value()
|
||||
);
|
||||
})
|
||||
|
||||
// Symlink index.html to README.html
|
||||
.then(function() {
|
||||
return fs.symlink(
|
||||
path.join(output, 'README.html'),
|
||||
path.join(output, 'index.html')
|
||||
);
|
||||
})
|
||||
|
||||
// Copy assets
|
||||
.then(function() {
|
||||
return fs.copy(
|
||||
path.join(__dirname, "../../assets/static"),
|
||||
path.join(output, "gitbook")
|
||||
);
|
||||
return options;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generators: generators,
|
||||
folder: generate
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
|
||||
var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../generator");
|
||||
|
||||
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.transferFile = function(input) {
|
||||
// ignore
|
||||
};
|
||||
|
||||
Generator.prototype.convertFile = function(content, input) {
|
||||
var that = this;
|
||||
var json = {
|
||||
progress: parse.progress(this.options.navigation, input)
|
||||
};
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
return parse.page(content, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(input) || '/'
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
json.sections = sections;
|
||||
})
|
||||
.then(function() {
|
||||
return fs.writeFile(
|
||||
path.join(that.options.output, input.replace(".md", ".json")),
|
||||
JSON.stringify(json, null, 4)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
// ignore
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -0,0 +1,84 @@
|
||||
var _ = require("lodash");
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var swig = require('swig');
|
||||
var hljs = require('highlight.js');
|
||||
|
||||
var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../site");
|
||||
|
||||
// Swig filter: highlight coloration
|
||||
swig.setFilter('code', function(code, lang) {
|
||||
try {
|
||||
return hljs.highlight(lang, code).value;
|
||||
} catch(e) {
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* This generator will generate a simple index.html which can be converted as a PDF
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Load base template
|
||||
this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/page.html'));
|
||||
|
||||
// List of pages content
|
||||
this.pages = {};
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.convertFile = function(content, input) {
|
||||
var that = this;
|
||||
var json = {
|
||||
path: input,
|
||||
progress: parse.progress(this.options.navigation, input)
|
||||
};
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
return parse.page(content, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(input) || '/',
|
||||
outdir: './',
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
json.content = sections;
|
||||
})
|
||||
.then(function() {
|
||||
that.pages[input] = json;
|
||||
});
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
var that = this;
|
||||
var basePath = ".";
|
||||
var output = path.join(this.options.output, "index.html");
|
||||
|
||||
return Q()
|
||||
// Generate html
|
||||
.then(function(pages) {
|
||||
return that._writeTemplate(that.template, {
|
||||
pages: that.pages,
|
||||
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, output);
|
||||
})
|
||||
|
||||
// Copy assets
|
||||
.then(function() {
|
||||
return fs.copy(
|
||||
path.join(that.options.theme, "assets"),
|
||||
path.join(that.options.output, "gitbook")
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -0,0 +1,56 @@
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../page");
|
||||
|
||||
/*
|
||||
* This generator inherits from the single page generator
|
||||
* and convert the page output to pdf using gitbook-pdf
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Options for PDF generation
|
||||
this.options = _.defaults(this.options, {
|
||||
paperformat: "A4"
|
||||
});
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
var that = this;
|
||||
|
||||
return BaseGenerator.prototype.finish.apply(this)
|
||||
.then(function() {
|
||||
var d = Q.defer();
|
||||
|
||||
var command = [
|
||||
"gitbook-pdf",
|
||||
"generate",
|
||||
path.join(that.options.output, "index.html"),
|
||||
path.join(that.options.output, "index.pdf"),
|
||||
"--format="+that.options.paperformat
|
||||
].join(" ");
|
||||
|
||||
exec(command, function (error, stdout, stderr) {
|
||||
if (error) {
|
||||
if (error.code == 127) {
|
||||
error.message = "Need to install gitbook-pdf using: npm install gitbook-pdf -g";
|
||||
} else {
|
||||
error.message = error.message + " "+stdout;
|
||||
}
|
||||
return d.reject(error);
|
||||
}
|
||||
d.resolve();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -0,0 +1,156 @@
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
var swig = require('swig');
|
||||
|
||||
var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../generator");
|
||||
|
||||
var indexer = require('./search_indexer');
|
||||
|
||||
// Swig filter for returning the count of lines in a code section
|
||||
swig.setFilter('lines', function(content) {
|
||||
return content.split('\n').length;
|
||||
});
|
||||
|
||||
// Swig filter for returning a link to the associated html file of a markdown file
|
||||
swig.setFilter('mdLink', function(link) {
|
||||
var link = link.replace(".md", ".html");
|
||||
if (link == "README.html") link = "index.html";
|
||||
return link;
|
||||
});
|
||||
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Attach methods to instance
|
||||
_.bindAll(this);
|
||||
|
||||
this.revision = Date.now();
|
||||
this.indexer = indexer();
|
||||
|
||||
// Load base template
|
||||
this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/site.html'));
|
||||
this.langsTemplate = swig.compileFile(path.resolve(this.options.theme, 'templates/langs.html'));
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
|
||||
// Generate a template
|
||||
Generator.prototype._writeTemplate = function(tpl, options, output) {
|
||||
var that = this;
|
||||
return Q()
|
||||
.then(function(sections) {
|
||||
return tpl(_.extend({
|
||||
revision: that.revision,
|
||||
|
||||
title: that.options.title,
|
||||
description: that.options.description,
|
||||
|
||||
githubAuthor: that.options.github.split("/")[0],
|
||||
githubId: that.options.github,
|
||||
githubHost: that.options.githubHost,
|
||||
|
||||
summary: that.options.summary,
|
||||
allNavigation: that.options.navigation
|
||||
}, options));
|
||||
})
|
||||
.then(function(html) {
|
||||
return fs.writeFile(
|
||||
output,
|
||||
html
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Generator.prototype.indexPage = function(lexed, pagePath) {
|
||||
this.indexer.add(lexed, pagePath);
|
||||
return Q();
|
||||
};
|
||||
|
||||
// Convert a markdown file to html
|
||||
Generator.prototype.convertFile = function(content, _input) {
|
||||
var that = this;
|
||||
var progress = parse.progress(this.options.navigation, _input);
|
||||
|
||||
_output = _input.replace(".md", ".html");
|
||||
if (_output == "README.html") _output = "index.html";
|
||||
|
||||
var input = path.join(this.options.input, _input);
|
||||
var output = path.join(this.options.output, _output);
|
||||
var basePath = path.relative(path.dirname(output), this.options.output) || ".";
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
// Lex page
|
||||
return parse.lex(content);
|
||||
})
|
||||
.then(function(lexed) {
|
||||
// Index page in search
|
||||
return that.indexPage(lexed, _output)
|
||||
.then(_.constant(lexed));
|
||||
})
|
||||
.then(function(lexed) {
|
||||
// Get HTML generated sections
|
||||
return parse.page(lexed, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(_input) || '/',
|
||||
outdir: path.dirname(_input) || '/',
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
return that._writeTemplate(that.template, {
|
||||
progress: progress,
|
||||
|
||||
_input: _input,
|
||||
content: sections,
|
||||
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, output);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate languages index
|
||||
Generator.prototype.langsIndex = function(langs) {
|
||||
var that = this;
|
||||
var basePath = ".";
|
||||
|
||||
return this._writeTemplate(this.langsTemplate, {
|
||||
langs: langs.list,
|
||||
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, path.join(this.options.output, "index.html"))
|
||||
.then(function() {
|
||||
// Copy assets
|
||||
return that.copyAssets();
|
||||
});
|
||||
};
|
||||
|
||||
// Copy assets
|
||||
Generator.prototype.copyAssets = function() {
|
||||
var that = this;
|
||||
|
||||
return fs.copy(
|
||||
path.join(that.options.theme, "assets"),
|
||||
path.join(that.options.output, "gitbook")
|
||||
);
|
||||
};
|
||||
|
||||
// Dump search index to disk
|
||||
Generator.prototype.writeSearchIndex = function() {
|
||||
return fs.writeFile(
|
||||
path.join(this.options.output, 'search_index.json'),
|
||||
this.indexer.dump()
|
||||
);
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
return this.copyAssets()
|
||||
.then(this.writeSearchIndex);
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -0,0 +1,71 @@
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
|
||||
var lunr = require('lunr');
|
||||
var marked = require('marked');
|
||||
var textRenderer = require('marked-text-renderer');
|
||||
|
||||
|
||||
function Indexer() {
|
||||
if(!(this instanceof Indexer)) {
|
||||
return new Indexer();
|
||||
}
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
// Setup lunr index
|
||||
this.idx = lunr(function () {
|
||||
this.ref('url');
|
||||
|
||||
this.field('title', { boost: 10 });
|
||||
this.field('body');
|
||||
});
|
||||
|
||||
this.renderer = textRenderer();
|
||||
}
|
||||
|
||||
Indexer.prototype.text = function(nodes) {
|
||||
// Copy section
|
||||
var section = _.toArray(nodes);
|
||||
|
||||
// marked's Render expects this, we don't use it yet
|
||||
section.links = {};
|
||||
|
||||
var options = _.extend({}, marked.defaults, {
|
||||
renderer: this.renderer
|
||||
});
|
||||
|
||||
return marked.parser(section, options);
|
||||
};
|
||||
|
||||
Indexer.prototype.addSection = function(path, section) {
|
||||
var url = [path, section.id].join('#');
|
||||
|
||||
var title = this.text(
|
||||
_.filter(section, {'type': 'heading'})
|
||||
);
|
||||
|
||||
var body = this.text(
|
||||
_.omit(section, {'type': 'heading'})
|
||||
);
|
||||
|
||||
// Add to lunr index
|
||||
this.idx.add({
|
||||
url: url,
|
||||
title: title,
|
||||
body: body,
|
||||
});
|
||||
};
|
||||
|
||||
Indexer.prototype.add = function(lexedPage, url) {
|
||||
var sections = lexedPage;
|
||||
|
||||
_.map(sections, _.partial(this.addSection, url));
|
||||
};
|
||||
|
||||
Indexer.prototype.dump = function() {
|
||||
return JSON.stringify(this.idx);
|
||||
};
|
||||
|
||||
// Exports
|
||||
module.exports = Indexer;
|
||||
@@ -1,67 +0,0 @@
|
||||
var swig = require('swig');
|
||||
var path = require('path');
|
||||
var _ = require("lodash");
|
||||
|
||||
var parse = require("../parse");
|
||||
|
||||
var fs = require('./fs');
|
||||
|
||||
swig.setFilter('lines', function(content) {
|
||||
return content.split('\n').length;
|
||||
});
|
||||
|
||||
// return a templeter for page
|
||||
var initTemplate = function(options) {
|
||||
var tpl = swig.compileFile(path.resolve(__dirname, '../../templates/page.html'));
|
||||
var nExercise = 1;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
// Base folder for input
|
||||
root: "./",
|
||||
|
||||
// Base folder for output
|
||||
output: "./",
|
||||
|
||||
// Locals for templates
|
||||
locals: {}
|
||||
});
|
||||
|
||||
return function(input, output, local) {
|
||||
var _input = input, _output = output;
|
||||
input = path.join(options.root, input);
|
||||
output = path.join(options.output, output);
|
||||
|
||||
var basePath = path.relative(path.dirname(output), options.output) || ".";
|
||||
|
||||
// Read markdown file
|
||||
return fs.readFile(input, "utf-8")
|
||||
|
||||
// Parse sections
|
||||
.then(function(markdown) {
|
||||
return parse.page(markdown, {
|
||||
repo: options.locals.githubId,
|
||||
dir: path.dirname(_input) || '/'
|
||||
});
|
||||
})
|
||||
|
||||
//Calcul template
|
||||
.then(function(sections) {
|
||||
return tpl(
|
||||
_.extend(local || {}, options.locals, {
|
||||
_input: _input,
|
||||
content: sections,
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
progress: parse.progress(options.locals.allNavigation, _output)
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
// Write html
|
||||
.then(function(html) {
|
||||
return fs.writeFile(output, html);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = initTemplate;
|
||||
@@ -1,6 +1,8 @@
|
||||
module.exports = {
|
||||
summary: require('./summary'),
|
||||
langs: require('./langs'),
|
||||
page: require('./page'),
|
||||
lex: require('./lex'),
|
||||
progress: require('./progress'),
|
||||
navigation: require('./navigation'),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
var _ = require("lodash")
|
||||
var parseSummary = require("./summary");
|
||||
|
||||
var parseLangs = function(content) {
|
||||
var summary = parseSummary(content);
|
||||
|
||||
return {
|
||||
list: _.chain(summary.chapters)
|
||||
.filter(function(entry) {
|
||||
return entry.path != null;
|
||||
})
|
||||
.map(function(entry) {
|
||||
return {
|
||||
title: entry.title,
|
||||
path: entry.path,
|
||||
lang: entry.path.replace("/", "")
|
||||
};
|
||||
})
|
||||
.value()
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = parseLangs;
|
||||
@@ -0,0 +1,77 @@
|
||||
var _ = require('lodash');
|
||||
var marked = require('marked');
|
||||
|
||||
// Split a page up into sections (lesson, exercises, ...)
|
||||
function splitSections(nodes) {
|
||||
var section = [];
|
||||
|
||||
return _.reduce(nodes, function(sections, el) {
|
||||
if(el.type === 'hr') {
|
||||
sections.push(section);
|
||||
section = [];
|
||||
} else {
|
||||
section.push(el);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}, []).concat([section]); // Add remaining nodes
|
||||
}
|
||||
|
||||
// What is the type of this section
|
||||
function sectionType(nodes, idx) {
|
||||
var codeNodes = _.filter(nodes, {
|
||||
type: 'code'
|
||||
}).length;
|
||||
|
||||
if(
|
||||
(codeNodes === 3 || codeNodes === 4) &&
|
||||
(idx % 2) === 1)
|
||||
{
|
||||
return 'exercise';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Generate a uniqueId to identify this section in our code
|
||||
function sectionId(section, idx) {
|
||||
return _.uniqueId('gitbook_');
|
||||
}
|
||||
|
||||
function lexPage(src) {
|
||||
// Lex file
|
||||
var nodes = marked.lexer(src);
|
||||
|
||||
return _.chain(splitSections(nodes))
|
||||
.map(function(section, idx) {
|
||||
// Detect section type
|
||||
section.type = sectionType(section, idx);
|
||||
return section;
|
||||
})
|
||||
.map(function(section, idx) {
|
||||
// Give each section an ID
|
||||
section.id = sectionId(section, idx);
|
||||
return section;
|
||||
|
||||
})
|
||||
.filter(function(section) {
|
||||
return !_.isEmpty(section);
|
||||
})
|
||||
.reduce(function(sections, section) {
|
||||
var last = _.last(sections);
|
||||
|
||||
// Merge normal sections together
|
||||
if(last && last.type === section.type && last.type === 'normal') {
|
||||
last.push.apply(last, [{'type': 'hr'}].concat(section));
|
||||
} else {
|
||||
// Add to list of sections
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}, [])
|
||||
.value();
|
||||
}
|
||||
|
||||
// Exports
|
||||
module.exports = lexPage;
|
||||
@@ -1,15 +1,15 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
// Cleans up an article/chapter object
|
||||
// remove 'articles' and '_path' attributes
|
||||
// remove 'articles' attributes
|
||||
function clean(obj) {
|
||||
return obj && _.omit(obj, ['articles', '_path']);
|
||||
return obj && _.omit(obj, ['articles']);
|
||||
}
|
||||
|
||||
// Returns a map of
|
||||
// Returns from a summary a map of
|
||||
/*
|
||||
{
|
||||
"file/path.html": {
|
||||
"file/path.md": {
|
||||
prev: ...,
|
||||
next: ...,
|
||||
},
|
||||
@@ -25,7 +25,7 @@ function navigation(summary, files) {
|
||||
|
||||
// Special README nav
|
||||
var README_NAV = {
|
||||
path: 'README.html',
|
||||
path: 'README.md',
|
||||
title: 'Introduction',
|
||||
};
|
||||
|
||||
@@ -69,7 +69,7 @@ function navigation(summary, files) {
|
||||
});
|
||||
|
||||
// Hack for README.html
|
||||
mapping['README.html'] = {
|
||||
mapping['README.md'] = {
|
||||
title: README_NAV.title,
|
||||
prev: null,
|
||||
next: clean(summary.chapters[0]),
|
||||
|
||||
@@ -2,8 +2,12 @@ var _ = require('lodash');
|
||||
var marked = require('marked');
|
||||
var hljs = require('highlight.js');
|
||||
|
||||
var lex = require('./lex');
|
||||
var renderer = require('./renderer');
|
||||
|
||||
var lnormalize = require('../utils/lang').normalize;
|
||||
|
||||
|
||||
// Synchronous highlighting with highlight.js
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
@@ -16,37 +20,11 @@ marked.setOptions({
|
||||
});
|
||||
|
||||
|
||||
// Split a page up into sections (lesson, exercises, ...)
|
||||
function splitSections(nodes) {
|
||||
var section = [];
|
||||
|
||||
return _.reduce(nodes, function(sections, el) {
|
||||
if(el.type === 'hr') {
|
||||
sections.push(section);
|
||||
section = [];
|
||||
} else {
|
||||
section.push(el);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}, []).concat([section]); // Add remaining nodes
|
||||
}
|
||||
|
||||
// What is the type of this section
|
||||
function sectionType(nodes, idx) {
|
||||
var codeNodes = _.filter(nodes, {
|
||||
type: 'code'
|
||||
}).length;
|
||||
|
||||
if(codeNodes === 3 && (idx % 2) == 1) {
|
||||
return 'exercise';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Render a section using our custom renderer
|
||||
function render(section, _options) {
|
||||
// Copy section
|
||||
section = _.toArray(section);
|
||||
|
||||
// marked's Render expects this, we don't use it yet
|
||||
section.links = {};
|
||||
|
||||
@@ -61,35 +39,9 @@ function render(section, _options) {
|
||||
function parsePage(src, options) {
|
||||
options = options || {};
|
||||
|
||||
// Lex file
|
||||
var nodes = marked.lexer(src);
|
||||
|
||||
return _.chain(splitSections(nodes))
|
||||
.map(function(section, idx) {
|
||||
// Detect section type
|
||||
section.type = sectionType(section, idx);
|
||||
return section;
|
||||
})
|
||||
.filter(function(section) {
|
||||
return !_.isEmpty(section);
|
||||
})
|
||||
.reduce(function(sections, section) {
|
||||
var last = _.last(sections);
|
||||
|
||||
// Merge normal sections together
|
||||
if(last && last.type === section.type && last.type === 'normal') {
|
||||
last.push.apply(last, [{'type': 'hr'}].concat(section));
|
||||
} else {
|
||||
// Add to list of sections
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}, [])
|
||||
// Lex if not already lexed
|
||||
return (_.isArray(src) ? src : lex(src))
|
||||
.map(function(section) {
|
||||
// Generate a uniqueId to identify this section in our code
|
||||
var id = _.uniqueId('gitbook_');
|
||||
|
||||
// Transform given type
|
||||
if(section.type === 'exercise') {
|
||||
var nonCodeNodes = _.reject(section, {
|
||||
@@ -100,26 +52,39 @@ function parsePage(src, options) {
|
||||
'type': 'code'
|
||||
});
|
||||
|
||||
// Languages in code blocks
|
||||
var langs = _.pluck(codeNodes, 'lang').map(lnormalize);
|
||||
|
||||
// Check that they are all the same
|
||||
var validLangs = _.all(_.map(langs, function(lang) {
|
||||
return lang && lang === langs[0];
|
||||
}));
|
||||
|
||||
// Main language
|
||||
var lang = validLangs ? langs[0] : null;
|
||||
|
||||
return {
|
||||
id: id,
|
||||
id: section.id,
|
||||
type: section.type,
|
||||
content: render(nonCodeNodes),
|
||||
lang: lang,
|
||||
code: {
|
||||
base: codeNodes[0].text,
|
||||
solution: codeNodes[1].text,
|
||||
validation: codeNodes[2].text,
|
||||
// Context is optional
|
||||
context: codeNodes[3] ? codeNodes[3].text : null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Render normal pages
|
||||
return {
|
||||
id: id,
|
||||
id: section.id,
|
||||
type: section.type,
|
||||
content: render(section, options)
|
||||
};
|
||||
})
|
||||
.value();
|
||||
});
|
||||
}
|
||||
|
||||
// Exports
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var _ = require("lodash");
|
||||
|
||||
// Returns from a navigation and a current file, a snapshot of current detailed state
|
||||
var calculProgress = function(navigation, current) {
|
||||
var n = _.size(navigation);
|
||||
var percent = 0, prevPercent = 0, currentChapter = null;
|
||||
|
||||
@@ -45,7 +45,7 @@ GitBookRenderer.prototype.link = function(href, title, text) {
|
||||
|
||||
var o = this._extra_options;
|
||||
// Relative link, rewrite it to point to github repo
|
||||
if(parsed.path[0] != '/' && o && o.repo && o.dir) {
|
||||
if(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.repo && o.dir) {
|
||||
href = 'https://github.com/' + o.repo + '/blob' + path.normalize(path.join(
|
||||
'/',
|
||||
o.dir,
|
||||
@@ -68,5 +68,27 @@ GitBookRenderer.prototype.link = function(href, title, text) {
|
||||
return out;
|
||||
};
|
||||
|
||||
GitBookRenderer.prototype.image = function(href, title, text) {
|
||||
// Our "fixed" href
|
||||
var _href = href;
|
||||
|
||||
// Parsed version of the url
|
||||
var parsed = url.parse(href);
|
||||
|
||||
// Options
|
||||
var o = this._extra_options;
|
||||
|
||||
// Relative image, rewrite it depending output
|
||||
if(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.dir && o.outdir) {
|
||||
_href = path.relative(o.outdir, path.normalize(path.join(
|
||||
o.dir,
|
||||
href
|
||||
)));
|
||||
}
|
||||
|
||||
return GitBookRenderer.super_.prototype.image.call(this, _href, title, text);
|
||||
};
|
||||
|
||||
|
||||
// Exports
|
||||
module.exports = GitBookRenderer;
|
||||
|
||||
@@ -82,10 +82,7 @@ function parseTitle(src, nums) {
|
||||
level: level,
|
||||
|
||||
// Replace .md references with .html
|
||||
path: matches[2].replace(/\.md$/, '.html'),
|
||||
|
||||
// Original, non normalized path
|
||||
_path: matches[2],
|
||||
path: matches[2],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
lang: require('./lang'),
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
var MAP = {
|
||||
'py': 'python',
|
||||
'js': 'javascript',
|
||||
'rb': 'ruby',
|
||||
};
|
||||
|
||||
function normalize(lang) {
|
||||
var lower = lang.toLowerCase();
|
||||
return MAP[lower] || lower;
|
||||
}
|
||||
|
||||
// Exports
|
||||
module.exports = {
|
||||
normalize: normalize,
|
||||
MAP: MAP
|
||||
};
|
||||
@@ -1,18 +1,21 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.3",
|
||||
"description": "Library and cmd utility to generate GitBooks",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"q": "1.0.1",
|
||||
"lodash": "2.4.1",
|
||||
"marked": "0.3.2",
|
||||
"marked-text-renderer": "0.0.1",
|
||||
"lunr": "0.5.2",
|
||||
"swig": "1.3.2",
|
||||
"send": "0.2.0",
|
||||
"fstream-ignore": "0.0.7",
|
||||
"commander": "2.2.0",
|
||||
"fs-extra": "0.8.1",
|
||||
"highlight.js": "8.0.0"
|
||||
"highlight.js": "8.0.0",
|
||||
"tmp": "0.0.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.18.2",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div class="book-header">
|
||||
<!-- Actions Left -->
|
||||
<a href="https://github.com/{{ githubId }}" target="_blank" class="btn pull-left"><i class="fa fa-github-alt"></i></a>
|
||||
<a href="#" class="btn pull-left toggle-summary"><i class="fa fa-align-justify"></i></a>
|
||||
|
||||
<!-- Actions Right -->
|
||||
<a href="#" target="_blank" class="btn pull-right" data-sharing="google-plus"><i class="fa fa-google-plus"></i></a>
|
||||
<a href="#" target="_blank" class="btn pull-right" data-sharing="facebook"><i class="fa fa-facebook"></i></a>
|
||||
<a href="#" target="_blank" class="btn pull-right" data-sharing="twitter"><i class="fa fa-twitter"></i></a>
|
||||
|
||||
<a href="https://github.com/{{ githubId }}/stargazers" target="_blank" class="btn pull-right count-star"><i class="fa fa-star-o"></i> Star (<span>-</span>)</a>
|
||||
<a href="https://github.com/{{ githubId }}/watchers" target="_blank" class="btn pull-right count-watch"><i class="fa fa-eye"></i> Watch (<span>-</span>)</a>
|
||||
|
||||
|
||||
<!-- Title -->
|
||||
<h1><a href="{{ basePath }}/README.html" >{{ title }}</a></h1>
|
||||
</div>
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}{{ progress.current.title }}{% parent %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="book with-summary" data-github="{{ githubId }}" data-level="{{ progress.current.level }}">
|
||||
{% include "includes/book/header.html" %}
|
||||
{% include "includes/book/summary.html" %}
|
||||
<div class="book-body" tabindex="-1">
|
||||
<div class="page-wrapper">
|
||||
<div class="book-progress">
|
||||
<div class="bar">
|
||||
<div class="inner" style="width: {{ progress.percent }}%;min-width: {{ progress.prevPercent }}%;"></div>
|
||||
</div>
|
||||
<div class="chapters">
|
||||
{% for p in progress.chapters %}
|
||||
<a href="{{ basePath }}/{{ p.path }}" title="{{ p.title }}" class="chapter {% if p.done %}done{% endif %} {% if p.level.length == 1 %}new-chapter{% endif %}" data-progress="{{ p.level }}" style="left: {{ p.percent }}%;"></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-inner">
|
||||
{% for section in content %}
|
||||
<section class="{{ section.type }}" id="section-{{ section.id }}">
|
||||
{% if section.type == "normal" %}
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
{% elif section.type == "exercise" %}
|
||||
{% include "./includes/book/exercise.html" with {section: section} %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="page-footer">
|
||||
{% if _input == "README.md" %}
|
||||
<a href="{{ basePath }}/{{ progress.current.next.path }}" class="navigation-link">Start</a>
|
||||
{% else %}
|
||||
{% if progress.current.next %}
|
||||
{% if progress.current.next.path %}
|
||||
<a href="{{ basePath }}/{{ progress.current.next.path }}" class="navigation-link next">Next</a>
|
||||
{% else %}
|
||||
<div class="navigation-link coming-soon">Coming soon</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="navigation-link finished">Finished!</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -30,3 +30,36 @@ Some more nice content ....
|
||||
[Cool stuff](http://gitbook.io)
|
||||
|
||||
[Link to another Markdown file](./xyz/file.md)
|
||||
|
||||
Lets go for another exercise but this time with some context :
|
||||
|
||||
---
|
||||
|
||||
Exercise with some context code :
|
||||
|
||||
Using the `double` function provided, build a `quadruple` function
|
||||
|
||||
```py
|
||||
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
def quadruple(x):
|
||||
return double(double(x))
|
||||
|
||||
```
|
||||
|
||||
```py
|
||||
assert(quadruple(8), 32)
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
def double(x):
|
||||
return x * 2
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -13,57 +13,57 @@ var LEXED = summary(CONTENT);
|
||||
describe('Summary navigation', function() {
|
||||
it('should provide next & prev entries for a file', function() {
|
||||
var nav = navigation(LEXED, [
|
||||
'README.html',
|
||||
'chapter-1/README.html',
|
||||
'chapter-1/ARTICLE1.html',
|
||||
'chapter-1/ARTICLE2.html',
|
||||
'chapter-2/README.html',
|
||||
'README.md',
|
||||
'chapter-1/README.md',
|
||||
'chapter-1/ARTICLE1.md',
|
||||
'chapter-1/ARTICLE2.md',
|
||||
'chapter-2/README.md',
|
||||
]);
|
||||
|
||||
// Make sure it found the files we gave it
|
||||
assert(nav['README.html']);
|
||||
assert(nav['chapter-1/README.html']);
|
||||
assert(nav['chapter-1/ARTICLE1.html']);
|
||||
assert(nav['chapter-1/ARTICLE2.html']);
|
||||
assert(nav['chapter-2/README.html']);
|
||||
assert(nav['README.md']);
|
||||
assert(nav['chapter-1/README.md']);
|
||||
assert(nav['chapter-1/ARTICLE1.md']);
|
||||
assert(nav['chapter-1/ARTICLE2.md']);
|
||||
assert(nav['chapter-2/README.md']);
|
||||
|
||||
|
||||
assert.equal(nav['README.html'].prev, null);
|
||||
assert.equal(nav['README.html'].next.path, 'chapter-1/README.html');
|
||||
assert.equal(nav['README.md'].prev, null);
|
||||
assert.equal(nav['README.md'].next.path, 'chapter-1/README.md');
|
||||
|
||||
assert.equal(nav['chapter-1/README.html'].prev.path, 'README.html');
|
||||
assert.equal(nav['chapter-1/README.html'].next.path, 'chapter-1/ARTICLE1.html');
|
||||
assert.equal(nav['chapter-1/README.md'].prev.path, 'README.md');
|
||||
assert.equal(nav['chapter-1/README.md'].next.path, 'chapter-1/ARTICLE1.md');
|
||||
|
||||
assert.equal(nav['chapter-1/ARTICLE1.html'].prev.path, 'chapter-1/README.html');
|
||||
assert.equal(nav['chapter-1/ARTICLE1.html'].next.path, 'chapter-1/ARTICLE2.html');
|
||||
assert.equal(nav['chapter-1/ARTICLE1.md'].prev.path, 'chapter-1/README.md');
|
||||
assert.equal(nav['chapter-1/ARTICLE1.md'].next.path, 'chapter-1/ARTICLE2.md');
|
||||
|
||||
assert.equal(nav['chapter-1/ARTICLE2.html'].prev.path, 'chapter-1/ARTICLE1.html');
|
||||
assert.equal(nav['chapter-1/ARTICLE2.html'].next.path, 'chapter-2/README.html');
|
||||
assert.equal(nav['chapter-1/ARTICLE2.md'].prev.path, 'chapter-1/ARTICLE1.md');
|
||||
assert.equal(nav['chapter-1/ARTICLE2.md'].next.path, 'chapter-2/README.md');
|
||||
|
||||
assert.equal(nav['chapter-2/README.html'].prev.path, 'chapter-1/ARTICLE2.html');
|
||||
assert.equal(nav['chapter-2/README.html'].next.path, 'chapter-3/README.html');
|
||||
assert.equal(nav['chapter-2/README.md'].prev.path, 'chapter-1/ARTICLE2.md');
|
||||
assert.equal(nav['chapter-2/README.md'].next.path, 'chapter-3/README.md');
|
||||
});
|
||||
|
||||
it('should give full tree, when not limited', function() {
|
||||
var nav = navigation(LEXED);
|
||||
|
||||
assert(nav['README.html']);
|
||||
assert(nav['chapter-1/README.html']);
|
||||
assert(nav['chapter-1/ARTICLE1.html']);
|
||||
assert(nav['chapter-1/ARTICLE2.html']);
|
||||
assert(nav['chapter-2/README.html']);
|
||||
assert(nav['chapter-3/README.html']);
|
||||
assert(nav['README.md']);
|
||||
assert(nav['chapter-1/README.md']);
|
||||
assert(nav['chapter-1/ARTICLE1.md']);
|
||||
assert(nav['chapter-1/ARTICLE2.md']);
|
||||
assert(nav['chapter-2/README.md']);
|
||||
assert(nav['chapter-3/README.md']);
|
||||
});
|
||||
|
||||
it('should detect levels correctly', function() {
|
||||
var nav = navigation(LEXED);
|
||||
|
||||
assert.equal(nav['README.html'].level, '0');
|
||||
assert.equal(nav['chapter-1/README.html'].level, '1');
|
||||
assert.equal(nav['chapter-1/ARTICLE1.html'].level, '1.1');
|
||||
assert.equal(nav['chapter-1/ARTICLE2.html'].level, '1.2');
|
||||
assert.equal(nav['chapter-2/README.html'].level, '2');
|
||||
assert.equal(nav['chapter-3/README.html'].level, '3');
|
||||
assert.equal(nav['README.md'].level, '0');
|
||||
assert.equal(nav['chapter-1/README.md'].level, '1');
|
||||
assert.equal(nav['chapter-1/ARTICLE1.md'].level, '1.1');
|
||||
assert.equal(nav['chapter-1/ARTICLE2.md'].level, '1.2');
|
||||
assert.equal(nav['chapter-2/README.md'].level, '2');
|
||||
assert.equal(nav['chapter-3/README.md'].level, '3');
|
||||
});
|
||||
|
||||
it('should not accept null paths', function() {
|
||||
|
||||
@@ -16,7 +16,7 @@ var LINKS_CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/GITHUB_LINK
|
||||
|
||||
describe('Page parsing', function() {
|
||||
it('should detection sections', function() {
|
||||
assert.equal(LEXED.length, 3);
|
||||
assert.equal(LEXED.length, 4);
|
||||
});
|
||||
|
||||
it('should detection section types', function() {
|
||||
@@ -36,6 +36,14 @@ describe('Page parsing', function() {
|
||||
assert(LEXED[1].code.base);
|
||||
assert(LEXED[1].code.solution);
|
||||
assert(LEXED[1].code.validation);
|
||||
assert(LEXED[1].code.context === null);
|
||||
|
||||
assert(LEXED[3].content);
|
||||
assert(LEXED[3].code);
|
||||
assert(LEXED[3].code.base);
|
||||
assert(LEXED[3].code.solution);
|
||||
assert(LEXED[3].code.validation);
|
||||
assert(LEXED[3].code.context);
|
||||
});
|
||||
|
||||
it('should merge sections correctly', function() {
|
||||
@@ -45,6 +53,10 @@ describe('Page parsing', function() {
|
||||
// HRs inserted correctly
|
||||
assert.equal(HR_LEXED[0].content.match(/<hr>/g).length, 2);
|
||||
});
|
||||
|
||||
it('should detect an exercise\'s language', function() {
|
||||
assert.equal(LEXED[1].lang, 'python');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ var LEXED = summary(CONTENT);
|
||||
|
||||
|
||||
describe('Summary parsing', function () {
|
||||
console.log(LEXED);
|
||||
|
||||
it('should detect chapters', function() {
|
||||
assert.equal(LEXED.chapters.length, 5);
|
||||
@@ -37,12 +36,9 @@ describe('Summary parsing', function () {
|
||||
});
|
||||
|
||||
it('should normalize paths from .md to .html', function() {
|
||||
assert.equal(LEXED.chapters[0].path,'chapter-1/README.html');
|
||||
assert.equal(LEXED.chapters[0]._path,'chapter-1/README.md');
|
||||
assert.equal(LEXED.chapters[1].path,'chapter-2/README.html');
|
||||
assert.equal(LEXED.chapters[1]._path,'chapter-2/README.md');
|
||||
assert.equal(LEXED.chapters[2].path,'chapter-3/README.html');
|
||||
assert.equal(LEXED.chapters[2]._path,'chapter-3/README.md');
|
||||
assert.equal(LEXED.chapters[0].path,'chapter-1/README.md');
|
||||
assert.equal(LEXED.chapters[1].path,'chapter-2/README.md');
|
||||
assert.equal(LEXED.chapters[2].path,'chapter-3/README.md');
|
||||
});
|
||||
|
||||
it('should detect levels correctly', function() {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# GitBook Theme
|
||||
|
||||
For creating a theme for GitBook, just copy this folder. The theme folder should at least contains two folders: `assets` and `templates`.
|
||||
|
||||
The theme can be changed using the option ```--theme=<path>```
|
||||
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
h1,h2{page-break-after:avoid;page-break-before:auto}pre,blockquote{border:1px solid #999;page-break-inside:avoid}img{max-width:100%!important;page-break-inside:avoid}section{page-break-after:always}section#cover{padding:3cm 0;text-align:center}section#cover h1{font-size:1.5cm}section#summary{text-align:center}section#summary ul{font-size:.5cm;line-height:1.8em;padding:0;margin:0;list-style:none}section#summary>ul>li{margin-bottom:1cm}section#summary>ul>li>a{font-size:.6cm}section article{margin:1.5cm}section article.new-chapter{page-break-after:always;font-size:.6cm;text-align:center;padding:3cm 0;border-top:1px solid #ccc}@media print{section article.new-chapter{border:0}}section article .exercise{margin:1cm 0;padding:.4cm;page-break-inside:avoid;border:3px solid #ddd}section article .exercise .exercise-header{margin-bottom:.4cm;padding-bottom:.2cm;border-bottom:1px solid #ddd}
|
||||
@@ -8,21 +8,21 @@ require([
|
||||
"core/keyboard",
|
||||
"core/exercise",
|
||||
"core/progress",
|
||||
"core/sidebar"
|
||||
], function($, storage, analytic, sharing, _state, keyboard, exercise, progress, sidebar){
|
||||
"core/sidebar",
|
||||
"core/search"
|
||||
], function($, storage, analytic, sharing, state, keyboard, exercise, progress, sidebar, search){
|
||||
$(document).ready(function() {
|
||||
var state = _state();
|
||||
var $book = state.$book;
|
||||
|
||||
// Initialize storage
|
||||
storage.setBaseKey(state.githubId);
|
||||
|
||||
// Tract page view
|
||||
analytic.track("View");
|
||||
|
||||
// Init sidebar
|
||||
sidebar.init();
|
||||
|
||||
// Load search
|
||||
search.init();
|
||||
|
||||
// Init keyboard
|
||||
keyboard.init();
|
||||
|
||||
@@ -8,6 +8,7 @@ define([
|
||||
var prepareExercise = function($exercise) {
|
||||
var codeSolution = $exercise.find(".code-solution").text();
|
||||
var codeValidation = $exercise.find(".code-validation").text();
|
||||
var codeContext = $exercise.find(".code-context").text();
|
||||
|
||||
var editor = ace.edit($exercise.find(".editor").get(0));
|
||||
editor.setTheme("ace/theme/tomorrow");
|
||||
@@ -20,7 +21,7 @@ define([
|
||||
|
||||
analytic.track("exercise.submit");
|
||||
|
||||
execute(editor.getValue(), codeValidation, function(err, result) {
|
||||
execute("javascript", editor.getValue(), codeValidation, codeContext, function(err, result) {
|
||||
$exercise.toggleClass("return-error", err != null);
|
||||
$exercise.toggleClass("return-success", err == null);
|
||||
if (err) $exercise.find(".alert-danger").text(err.message || err);
|
||||
@@ -38,7 +39,7 @@ define([
|
||||
|
||||
// Prepare all exercise
|
||||
var init = function() {
|
||||
state().$book.find("section.exercise").each(function() {
|
||||
state.$book.find("section.exercise").each(function() {
|
||||
prepareExercise($(this));
|
||||
});
|
||||
};
|
||||
@@ -2,8 +2,9 @@ define([
|
||||
"jQuery",
|
||||
"Mousetrap",
|
||||
"core/navigation",
|
||||
"core/sidebar"
|
||||
], function($, Mousetrap, navigation, sidebar){
|
||||
"core/sidebar",
|
||||
"core/search"
|
||||
], function($, Mousetrap, navigation, sidebar, search){
|
||||
// Bind keyboard shortcuts
|
||||
var init = function() {
|
||||
// Next
|
||||
@@ -23,9 +24,16 @@ define([
|
||||
sidebar.toggle();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Toggle Search
|
||||
Mousetrap.bind(['f'], function(e) {
|
||||
search.toggle();
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
init: init,
|
||||
search: search
|
||||
};
|
||||
});
|
||||
@@ -6,7 +6,7 @@ define([
|
||||
], function(_, $, storage, state) {
|
||||
// Get current level
|
||||
var getCurrentLevel = function() {
|
||||
return state().level;
|
||||
return state.level;
|
||||
};
|
||||
|
||||
// Return all levels
|
||||
@@ -0,0 +1,97 @@
|
||||
define([
|
||||
"jQuery",
|
||||
"lodash",
|
||||
"lunr",
|
||||
"utils/storage",
|
||||
"core/state",
|
||||
"core/sidebar"
|
||||
], function($, _, lunr, storage, state, sidebar) {
|
||||
var index = null;
|
||||
var $searchBar = state.$book.find(".book-search");
|
||||
var $searchInput = $searchBar.find("input");
|
||||
|
||||
// Use a specific idnex
|
||||
var useIndex = function(data) {
|
||||
index = lunr.Index.load(data);
|
||||
};
|
||||
|
||||
// Load complete index
|
||||
var loadIndex = function() {
|
||||
$.getJSON(state.basePath+"/search_index.json")
|
||||
.then(useIndex);
|
||||
};
|
||||
|
||||
// Search for a term
|
||||
var search = function(q) {
|
||||
if (!index) return;
|
||||
var results = _.chain(index.search(q))
|
||||
.map(function(result) {
|
||||
var parts = result.ref.split("#")
|
||||
return {
|
||||
path: parts[0],
|
||||
hash: parts[1]
|
||||
}
|
||||
})
|
||||
.value();
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Toggle search bar
|
||||
var toggleSearch = function(_state) {
|
||||
if (state != null && isSearchOpen() == _state) return;
|
||||
|
||||
state.$book.toggleClass("with-search", _state);
|
||||
|
||||
// If search bar is open: focus input
|
||||
if (isSearchOpen()) {
|
||||
sidebar.toggle(true);
|
||||
$searchInput.focus();
|
||||
} else {
|
||||
$searchInput.blur();
|
||||
$searchInput.val("");
|
||||
sidebar.filter(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Return true if search bar is open
|
||||
var isSearchOpen = function() {
|
||||
return state.$book.hasClass("with-search");
|
||||
};
|
||||
|
||||
|
||||
var init = function() {
|
||||
loadIndex();
|
||||
|
||||
// Toggle search
|
||||
state.$book.find(".book-header .toggle-search").click(function(e) {
|
||||
e.preventDefault();
|
||||
toggleSearch();
|
||||
});
|
||||
|
||||
$searchInput.keyup(function(e) {
|
||||
var key = (e.keyCode ? e.keyCode : e.which);
|
||||
var q = $(this).val();
|
||||
|
||||
if (key == 27) {
|
||||
e.preventDefault();
|
||||
toggleSearch(false);
|
||||
return;
|
||||
}
|
||||
if (q.length == 0) {
|
||||
sidebar.filter(null);
|
||||
} else {
|
||||
var results = search(q);
|
||||
sidebar.filter(
|
||||
_.pluck(results, "path")
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
search: search,
|
||||
toggle: toggleSearch
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
define([
|
||||
"lodash",
|
||||
"utils/storage",
|
||||
"utils/platform",
|
||||
"core/state"
|
||||
], function(_, storage, platform, state) {
|
||||
var $summary = state.$book.find(".book-summary");
|
||||
|
||||
// Toggle sidebar with or withour animation
|
||||
var toggleSidebar = function(_state, animation) {
|
||||
if (state != null && isOpen() == _state) return;
|
||||
if (animation == null) animation = true;
|
||||
|
||||
state.$book.toggleClass("without-animation", !animation);
|
||||
state.$book.toggleClass("with-summary", _state);
|
||||
|
||||
storage.set("sidebar", isOpen());
|
||||
};
|
||||
|
||||
// Return true if sidebar is open
|
||||
var isOpen = function() {
|
||||
return state.$book.hasClass("with-summary");
|
||||
};
|
||||
|
||||
// Prepare sidebar: state and toggle button
|
||||
var init = function() {
|
||||
// Toggle summary
|
||||
state.$book.find(".book-header .toggle-summary").click(function(e) {
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
});
|
||||
|
||||
// Init last state if not mobile
|
||||
if (!platform.isMobile) {
|
||||
toggleSidebar(storage.get("sidebar", true), false);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter summary with a list of path
|
||||
var filterSummary = function(paths) {
|
||||
$summary.find("li").each(function() {
|
||||
var path = $(this).data("path");
|
||||
var st = paths == null || _.contains(paths, path);
|
||||
|
||||
$(this).toggle(st);
|
||||
if (st) $(this).parents("li").show();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
$el: $summary,
|
||||
init: init,
|
||||
toggle: toggleSidebar,
|
||||
filter: filterSummary
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
define([
|
||||
"jQuery"
|
||||
], function() {
|
||||
var $book = $(".book");
|
||||
|
||||
return {
|
||||
'$book': $book,
|
||||
|
||||
'githubId': $book.data("github"),
|
||||
'level': $book.data("level"),
|
||||
'basePath': $book.data("basepath"),
|
||||
'revision': $book.data("revision")
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
define(function() {
|
||||
return {
|
||||
id: "javascript",
|
||||
assertCode: "function assert(condition, message) { \nif (!condition) { \n throw message || \"Assertion failed\"; \n } \n }\n",
|
||||
REPL: JSREPL,
|
||||
sep: ";\n",
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
define([
|
||||
"lodash",
|
||||
"mixpanel"
|
||||
], function(_, mixpanel) {
|
||||
mixpanel.init("01eb2b950ae09a5fdb15a98dcc5ff20e", {
|
||||
loaded: function() {
|
||||
track("View");
|
||||
}
|
||||
});
|
||||
|
||||
var isAvailable = function() {
|
||||
return (
|
||||
typeof mixpanel !== "undefined" &&
|
||||
typeof mixpanel.track === "function"
|
||||
);
|
||||
};
|
||||
|
||||
var track = function(e, data) {
|
||||
if (!isAvailable()) {
|
||||
console.warn("tracking not available!");
|
||||
return;
|
||||
}
|
||||
console.log("track", e);
|
||||
mixpanel.track(e, _.extend(data || {}, {
|
||||
'domain': window.location.host
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
isAvailable: isAvailable,
|
||||
track: track
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,12 @@
|
||||
define(function(){
|
||||
var evalJS = function(code, callback) {
|
||||
define([
|
||||
"execute/javascript"
|
||||
], function(javascript) {
|
||||
var LANGUAGES = {
|
||||
"javascript": javascript
|
||||
};
|
||||
|
||||
|
||||
var evalJS = function(lang, code, callback) {
|
||||
var ready = false;
|
||||
var finished = false;
|
||||
|
||||
@@ -11,7 +18,7 @@ define(function(){
|
||||
return callback.apply(null, arguments);
|
||||
};
|
||||
|
||||
var jsrepl;
|
||||
var repl;
|
||||
|
||||
// Handles all our events
|
||||
var eventHandler = function(data, eventType) {
|
||||
@@ -42,14 +49,14 @@ define(function(){
|
||||
// We're good to get results and stuff back now
|
||||
ready = true;
|
||||
// Eval our code now that the runtime is ready
|
||||
jsrepl.eval(code);
|
||||
repl.eval(code);
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event =', eventType, 'data =', data);
|
||||
}
|
||||
};
|
||||
|
||||
jsrepl = new JSREPL({
|
||||
repl = new lang.REPL({
|
||||
input: eventHandler,
|
||||
output: eventHandler,
|
||||
result: eventHandler,
|
||||
@@ -61,16 +68,24 @@ define(function(){
|
||||
}
|
||||
});
|
||||
|
||||
jsrepl.loadLanguage('javascript', eventHandler);
|
||||
repl.loadLanguage(lang.id, eventHandler);
|
||||
};
|
||||
|
||||
var execute = function(lang, solution, validation, context, callback) {
|
||||
// Language data
|
||||
var langd = LANGUAGES[lang];
|
||||
|
||||
var ass = "function assert(condition, message) { \nif (!condition) { \n throw message || \"Assertion failed\"; \n } \n }\n";
|
||||
// Check language is supported
|
||||
if (!langd) return callback(new Error("Language '"+lang+"' not available for execution"));
|
||||
|
||||
var execute = function(solution, validation, callback) {
|
||||
// Validate with validation code
|
||||
var code = [solution, ass, validation].join(";\n");
|
||||
evalJS(code, function(err, res) {
|
||||
var code = [
|
||||
context,
|
||||
solution,
|
||||
langd.assertCode,
|
||||
validation,
|
||||
].join(langd.sep);
|
||||
evalJS(langd, code, function(err, res) {
|
||||
if(err) return callback(err);
|
||||
|
||||
if (res.type == "error") callback(new Error(res.value));
|
||||
@@ -79,4 +94,4 @@ define(function(){
|
||||
};
|
||||
|
||||
return execute;
|
||||
});
|
||||
});
|
||||
@@ -14,9 +14,12 @@ define(function(){
|
||||
},
|
||||
get: function(key, def) {
|
||||
key = baseKey+":"+key;
|
||||
if (localStorage[key] === undefined) return def;
|
||||
try {
|
||||
return JSON.parse(localStorage[key]) || def;
|
||||
var v = JSON.parse(localStorage[key]);
|
||||
return v == null ? def : v;;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
return localStorage[key] || def;
|
||||
}
|
||||
},
|
||||
@@ -0,0 +1,2 @@
|
||||
(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");
|
||||
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
|
||||
@@ -5,7 +5,15 @@
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
|
||||
.body-inner {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.transition(left 0.5s ease);
|
||||
|
||||
@@ -14,7 +22,6 @@
|
||||
.page-wrapper {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
padding-bottom: 100px;
|
||||
|
||||
.page-inner {
|
||||
max-width: 800px;
|
||||
@@ -39,6 +46,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @mobileMaxWidth) {
|
||||
overflow-y: auto;
|
||||
|
||||
.body-inner {
|
||||
position: static;
|
||||
padding-bottom: 20px;
|
||||
min-height: calc(~"100% - 57px")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.with-summary {
|
||||
@@ -0,0 +1,46 @@
|
||||
.book-langs-index {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #edece4;
|
||||
padding: 40px 0px;
|
||||
margin: 0px;
|
||||
overflow: auto;
|
||||
|
||||
.inner {
|
||||
max-width: 350px;
|
||||
width: 100%;
|
||||
|
||||
margin: 0px auto;
|
||||
padding: 15px;
|
||||
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.languages {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-top: 20px;
|
||||
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
li {
|
||||
text-align: center;
|
||||
padding: 10px 5px;
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
a {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,10 @@
|
||||
margin-bottom: 0; }
|
||||
|
||||
table {
|
||||
padding: 0; }
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-top: 1px solid #cccccc;
|
||||
background-color: white;
|
||||
@@ -181,7 +184,10 @@
|
||||
margin-bottom: 0; }
|
||||
|
||||
img {
|
||||
max-width: 100%; }
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
span.frame {
|
||||
display: block;
|
||||
@@ -0,0 +1,45 @@
|
||||
.book .book-body {
|
||||
.navigation {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
align-content:center;
|
||||
flex-direction:column;
|
||||
|
||||
font-size: 40px;
|
||||
color: rgba(0,0,0, 0.5);
|
||||
|
||||
text-align: center;
|
||||
|
||||
.transition(all 350ms ease);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0,0,0, 0.06);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.navigation-next {
|
||||
right: 0px;
|
||||
}
|
||||
&.navigation-prev {
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: @mobileMaxWidth) {
|
||||
.navigation {
|
||||
position: static;
|
||||
top: auto;
|
||||
max-width: 50%;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
position: relative;
|
||||
background: #fff;
|
||||
margin-bottom: 20px;
|
||||
z-index: 10;
|
||||
|
||||
.bar {
|
||||
height: 8px;
|
||||
@@ -1,4 +1,6 @@
|
||||
.book {
|
||||
@searchHeight: 41px;
|
||||
|
||||
.book-summary {
|
||||
@width: 250px;
|
||||
|
||||
@@ -8,19 +10,49 @@
|
||||
bottom: 0px;
|
||||
z-index: 1;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
width: @width;
|
||||
background: #2e3133;
|
||||
color: #e2edf2;
|
||||
|
||||
.transition(all 0.5s ease);
|
||||
|
||||
.book-search {
|
||||
padding: 6px;
|
||||
padding-top: 0px;
|
||||
|
||||
background: #242628;
|
||||
border-bottom: 1px solid #333;
|
||||
|
||||
position: absolute;
|
||||
top: -@searchHeight;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
.transition(top 0.5s ease);
|
||||
|
||||
|
||||
input, input:focus {
|
||||
width: 100%;
|
||||
border: none;
|
||||
.box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
ul.summary {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
|
||||
.transition(top 0.5s ease);
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
@@ -96,4 +128,16 @@
|
||||
.transition(none) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-search {
|
||||
.book-summary {
|
||||
.book-search {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
ul.summary {
|
||||
top: @searchHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
@import "highlight.less";
|
||||
|
||||
@import "book/languages.less";
|
||||
@import "book/header.less";
|
||||
@import "book/summary.less";
|
||||
@import "book/body.less";
|
||||
@import "book/footer.less";
|
||||
@import "book/exercise.less";
|
||||
@import "book/markdown.less";
|
||||
@import "book/progress.less";
|
||||
@import "book/navigation.less";
|
||||
|
||||
* {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -0,0 +1,81 @@
|
||||
h1, h2 {
|
||||
page-break-after: avoid;
|
||||
page-break-before: auto;
|
||||
}
|
||||
|
||||
pre, blockquote {
|
||||
border: 1px solid #999;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
section {
|
||||
page-break-after: always;
|
||||
|
||||
&#cover {
|
||||
padding: 3cm 0cm;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5cm;
|
||||
}
|
||||
}
|
||||
|
||||
&#summary {
|
||||
text-align: center;
|
||||
|
||||
ul {
|
||||
font-size: 0.5cm;
|
||||
line-height: 1.8em;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
> ul {
|
||||
> li {
|
||||
margin-bottom: 1cm;
|
||||
|
||||
> a {
|
||||
font-size: 0.6cm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
margin: 1.5cm;
|
||||
|
||||
/* Big centered title */
|
||||
&.new-chapter {
|
||||
page-break-after: always;
|
||||
font-size: 0.6cm;
|
||||
text-align: center;
|
||||
padding: 3cm 0cm;
|
||||
|
||||
border-top: 1px solid #ccc;
|
||||
|
||||
@media print {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.exercise {
|
||||
margin: 1cm 0cm;
|
||||
padding: 0.4cm;
|
||||
page-break-inside: avoid;
|
||||
|
||||
border: 3px solid #ddd;
|
||||
|
||||
.exercise-header {
|
||||
margin-bottom: 0.4cm;
|
||||
padding-bottom: 0.2cm;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
/* Base static */
|
||||
@staticPath: "./";
|
||||
|
||||
/* Colors */
|
||||
@codeboxMainColor: #F16956; /* red */
|
||||
@codeboxMainColorDarker: darken(@codeboxMainColor, 15%);
|
||||
// Sizes
|
||||
@mobileMaxWidth: 1240px;
|
||||
|
||||
@codeboxMainColor2: #353D48;
|
||||
|
||||
// Basics of a navbar
|
||||
@navbar-height: 50px;
|
||||
@@ -22,13 +20,6 @@
|
||||
@navbar-default-brand-hover-color: @navbar-default-link-color;
|
||||
@navbar-default-brand-hover-bg: transparent;
|
||||
|
||||
// Navbar toggle
|
||||
@navbar-default-toggle-hover-bg: #fff;
|
||||
@navbar-default-toggle-icon-bar-bg: @codeboxMainColorDarker;
|
||||
@navbar-default-toggle-border-color: @codeboxMainColorDarker;
|
||||
@navbar-default-link-active-color: @codeboxMainColor;
|
||||
@navbar-default-link-active-bg: #fff;
|
||||
|
||||
@border-radius-base: 1px;
|
||||
@border-radius-large: 3px;
|
||||
@border-radius-small: 1px;
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
// Reset
|
||||
@import "normalize.less";
|
||||
@import "print.less";
|
||||
//@import "print.less";
|
||||
|
||||
// Core CSS
|
||||
@import "scaffolding.less";
|
||||