mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 07:35:16 +00:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fd7732345 | |||
| 960ef4d945 | |||
| 92523fb4b5 | |||
| 66bb46df22 | |||
| 0454710d3b | |||
| c9a31cc987 | |||
| f03c316274 | |||
| 001a4f91be | |||
| 44a3bfe296 | |||
| a9ce6f808d | |||
| e5c667e0fa | |||
| e36f274dbe | |||
| 2a88427e74 | |||
| ed48f5f3ee | |||
| 7b4accb117 | |||
| d6437d31d4 | |||
| 3dd33f1f3c | |||
| 3a0f00411b | |||
| 5b655bb29a | |||
| faf10605e8 | |||
| 5c98b3350b | |||
| 6f4e1d10c1 | |||
| 870886599a | |||
| 1b9801b8a4 | |||
| 897463b22e | |||
| aa4894e94c | |||
| 8a0180ae6b | |||
| d6776b98d3 | |||
| 6a04ba89a5 | |||
| 05eaeeb0d0 | |||
| 77043464b2 | |||
| b90f74a821 | |||
| f804eb42ac | |||
| 74fd8c159b | |||
| a741ec1350 | |||
| 4b26c31b3b | |||
| dfd487692a | |||
| 195374ecee | |||
| 1659778a77 | |||
| 40c4035afb | |||
| 1502d65286 | |||
| 0feb672d70 | |||
| 2a4ef5bbf4 | |||
| c4540332a7 | |||
| 2104404cb3 | |||
| 002d685ebb | |||
| 9da10e2b25 | |||
| 1e4632080b | |||
| 5191106396 | |||
| 6ab5cafaf3 | |||
| d1c0cb9c71 | |||
| 38c750101c | |||
| ba0f379a89 | |||
| 1d9509b3b6 | |||
| 3c8de4a8ab | |||
| 591d8fc9ee | |||
| d480e9cea4 | |||
| 32a71869da | |||
| fdbfda64a6 | |||
| a6f48be39e | |||
| ce50b893e9 | |||
| 225ff27dd1 | |||
| 52e8361846 | |||
| 49e3bbc3d6 | |||
| c9ec911d0e | |||
| 84d7662fd8 | |||
| 676b2287ee | |||
| 32a64be407 | |||
| 138cddf3d6 | |||
| d730a3aac6 | |||
| ffb0b12012 | |||
| 78e25f8252 | |||
| 09f42f57e1 | |||
| 950ad1d060 | |||
| ed6b29c1a4 | |||
| 36785120b1 | |||
| 26fad5b888 | |||
| f049576575 | |||
| a7ecbb87e4 | |||
| 1da341741b | |||
| 2dd6192576 | |||
| 52c9347b9c | |||
| e2295591ed | |||
| 0c1cb724d7 | |||
| 7cee248426 | |||
| 6ea5b2b539 | |||
| a56b559d43 | |||
| df1a82e0e4 | |||
| f15a1efe71 | |||
| b8379fc6fa | |||
| d69cc74647 | |||
| f83874a9ca | |||
| 1968bb79c2 | |||
| 60c721db40 | |||
| 11b299653f | |||
| c8c07fe8ce | |||
| be0a4f08e9 | |||
| 4da4e28d07 | |||
| 9dc19026c6 | |||
| 31ca9f7fcb | |||
| 6925d39e8d | |||
| a24cafd975 | |||
| e3fdf8b404 | |||
| c861223378 | |||
| f5c6d8df71 | |||
| 452c846e90 | |||
| aedd875ab7 | |||
| d2ec2f8f02 | |||
| 5af0df93c3 |
+57
@@ -1,5 +1,62 @@
|
||||
# Release notes
|
||||
|
||||
## 1.4.1
|
||||
- Fix command 'install' without arguments
|
||||
|
||||
## 1.4.0
|
||||
- Add command `gitbook install` to install plugins from book.json
|
||||
- `package.json` is no longer necessary
|
||||
|
||||
## 1.3.4
|
||||
- Add glossary to ebooks
|
||||
- Fix autocover with new hook "finish:before"
|
||||
- Add X-UA-Compatible meta tag for IE
|
||||
|
||||
## 1.3.3
|
||||
- Fix parsing of lexed content using the client library
|
||||
|
||||
## 1.3.2
|
||||
- ePub files are now passing validation from epubcheck
|
||||
- Fix replacement of multiple glossary terms in a single sentence
|
||||
- Fix on windows deep relative links
|
||||
- Fix search indexer
|
||||
|
||||
## 1.3.1
|
||||
- Fix error with links in markdown
|
||||
|
||||
## 1.3.0
|
||||
- Bundle gitbook parsing library as a client side library in `gitbook.js` and `gitbook.min.js`
|
||||
|
||||
## 1.2.0
|
||||
- Improvements on ebook generation
|
||||
- Fix incorrect follow of links in ebook generation
|
||||
- Move Table of Contents at the beginning of the ebook
|
||||
- Update to last highlight.js (includes Swift)
|
||||
- Includes of templates and variables (from book.json)
|
||||
|
||||
## 1.1.1
|
||||
- Rewrite quiz logic to be more robust
|
||||
- Improve integration of glossary
|
||||
- Improve generation of ebook by using a multiple HTML pages input source
|
||||
- Fix incorrect page breaks after h1 and h2 divs
|
||||
- New options to set header and footer in PDF generation
|
||||
|
||||
## 1.1.0
|
||||
- Plugins can now extend the ebook generation (pdf, epub, mobi)
|
||||
- Update `kramed` to version 0.4.3
|
||||
|
||||
## 1.0.3
|
||||
- Update `mathjax` plugin and MathJAx to version 2.4
|
||||
- Update `highlight.js` to 8.2.0
|
||||
|
||||
## 1.0.2
|
||||
- Update `mathjax` plugin, fixes issues with inline math rendering (no longer wanted)
|
||||
|
||||
## 1.0.1
|
||||
- New inline math convention (kramdown's), using `$$` rather than `$` as delimiters
|
||||
- Fix instapaper sharing
|
||||
- The `exercises` & `quizzes` plugins are now by default
|
||||
|
||||
## 1.0.0
|
||||
- New design
|
||||
- Support for glossary
|
||||
|
||||
+34
-1
@@ -6,6 +6,8 @@ module.exports = function (grunt) {
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-contrib-requirejs');
|
||||
grunt.loadNpmTasks("grunt-bower-install-simple");
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
|
||||
// Init GRUNT configuraton
|
||||
grunt.initConfig({
|
||||
@@ -77,6 +79,30 @@ module.exports = function (grunt) {
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
browserify: {
|
||||
dist: {
|
||||
files: {
|
||||
'gitbook.js': [
|
||||
'./lib/parse/index.js'
|
||||
],
|
||||
},
|
||||
options: {
|
||||
postBundleCB: function (err, src, next) {
|
||||
return next(null, '(function () { var define = undefined; '+src+'; })();')
|
||||
},
|
||||
browserifyOptions: {
|
||||
'standalone': "gitbook"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
dist: {
|
||||
files: {
|
||||
'gitbook.min.js': ['gitbook.js']
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,7 +116,14 @@ module.exports = function (grunt) {
|
||||
'copy:vendors'
|
||||
]);
|
||||
|
||||
// Bundle the library
|
||||
grunt.registerTask('bundle', [
|
||||
'browserify',
|
||||
'uglify'
|
||||
]);
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'build'
|
||||
'build',
|
||||
'bundle'
|
||||
]);
|
||||
};
|
||||
@@ -16,6 +16,11 @@ GitBook can be installed from **NPM** using:
|
||||
$ npm install gitbook -g
|
||||
```
|
||||
|
||||
Create the directories and files for a book from its [SUMMARY.md](https://github.com/GitbookIO/gitbook#book-format) file using
|
||||
```
|
||||
$ gitbook init
|
||||
```
|
||||
|
||||
You can serve a repository as a book using:
|
||||
|
||||
```
|
||||
@@ -52,9 +57,10 @@ Here are the options that can be stored in this file:
|
||||
// It's not advised this option in the book.json
|
||||
"generator": "site",
|
||||
|
||||
// Book title and description (defaults are extracted from the README)
|
||||
// Book metadats (somes are extracted from the README by default)
|
||||
"title": null,
|
||||
"description": null,
|
||||
"isbn": null,
|
||||
|
||||
// For ebook format, the extension to use for generation (default is detected from output extension)
|
||||
// "epub", "pdf", "mobi"
|
||||
@@ -62,10 +68,6 @@ Here are the options that can be stored in this file:
|
||||
// It's not advised this option in the book.json
|
||||
"extension": null,
|
||||
|
||||
// GitHub information (defaults are extracted using git)
|
||||
"github": null,
|
||||
"githubHost": "https://github.com/",
|
||||
|
||||
// Plugins list, can contain "-name" for removing default plugins
|
||||
"plugins": [],
|
||||
|
||||
@@ -78,6 +80,9 @@ Here are the options that can be stored in this file:
|
||||
}
|
||||
},
|
||||
|
||||
// Variables for templating
|
||||
"variables": {},
|
||||
|
||||
// Links in template (null: default, false: remove, string: new value)
|
||||
"links": {
|
||||
// Custom links at top of sidebar
|
||||
@@ -98,9 +103,6 @@ Here are the options that can be stored in this file:
|
||||
|
||||
// Options for PDF generation
|
||||
"pdf": {
|
||||
// Add toc at the end of the file
|
||||
"toc": true,
|
||||
|
||||
// Add page numbers to the bottom of every page
|
||||
"pageNumbers": false,
|
||||
|
||||
@@ -118,7 +120,13 @@ Here are the options that can be stored in this file:
|
||||
"left": 62,
|
||||
"top": 36,
|
||||
"bottom": 36
|
||||
}
|
||||
},
|
||||
|
||||
//Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
||||
"headerTemplate": null,
|
||||
|
||||
//Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
||||
"footerTemplate": null
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -130,8 +138,10 @@ You can publish your books to our index by visiting [GitBook.io](http://www.gitb
|
||||
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.
|
||||
* **eBook**: A complete eBook with exercise solutions at the end of the book. Generate this format using: ```gitbook ebook ./myrepo```. You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. The output format could be **PDF**, **ePub** or **MOBI**.
|
||||
* **Single Page**: The book will be stored in a single printable HTML page. This format is used for conversion to PDF or eBook. Generate this format using: ```gitbook build ./myrepo -f page```.
|
||||
* **eBook**: A complete eBook with exercise solutions at the end of the book. You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. You can specify the eBook filename with the `-o` option, otherwise `book` will be used.
|
||||
* Generate a **PDF** using: `gitbook pdf ./myrepo`
|
||||
* Generate a **ePub** using: `gitbook epub ./myrepo`
|
||||
* Generate a **MOBI** using: `gitbook mobi ./myrepo`
|
||||
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate this format using: ```gitbook build ./myrepo -f json```.
|
||||
|
||||
## Book Format
|
||||
@@ -174,7 +184,7 @@ GitBook supports building books written in multiple languages. Each language sho
|
||||
|
||||
You can see a complete example with the [Learn Git](https://github.com/GitbookIO/git) book.
|
||||
|
||||
#### GLOSSARY.md
|
||||
#### Glossary
|
||||
|
||||
Allows you to specify terms and their respective definitions to be displayed in the glossary. Based on those terms, `gitbook` will automatically build an index and highlight those terms in pages.
|
||||
|
||||
@@ -193,7 +203,7 @@ With it's definition, this can contain bold text and all other kinds of inline m
|
||||
|
||||
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`).
|
||||
|
||||
Best practices for the `.gitignore` is to ignore build files from [**node.js**](https://github.com/github/gitignore/blob/master/Node.gitignore) (`node_modules`, ...) and build files from GitBook: `_book`, `*.epub`, `*.mobi` and `*.pdf`.
|
||||
Best practices for the `.gitignore` is to ignore build files from **node.js** (`node_modules`, ...) and build files from GitBook: `_book`, `*.epub`, `*.mobi` and `*.pdf` ([Download GitBook.gitignore](https://github.com/github/gitignore/blob/master/GitBook.gitignore)).
|
||||
|
||||
#### Cover
|
||||
|
||||
@@ -208,7 +218,9 @@ The platform [GitBook.io](https://www.gitbook.io/) is like an "Heroku for books"
|
||||
|
||||
#### Plugins
|
||||
|
||||
Plugins can used to extend your book's functionality. Read [GitbookIO/plugin](https://github.com/GitbookIO/plugin) for more information about how to build a plugin for GitBook.
|
||||
Plugins can be used to extend your book's functionality. Read [GitbookIO/plugin](https://github.com/GitbookIO/plugin) for more information about how to build a plugin for GitBook.
|
||||
|
||||
Plugins needed to build a book can be installed using: `gitbook install ./`.
|
||||
|
||||
##### Official plugins:
|
||||
|
||||
@@ -218,6 +230,7 @@ Plugins can used to extend your book's functionality. Read [GitbookIO/plugin](ht
|
||||
| [quizzes](https://github.com/GitbookIO/plugin-quizzes) | Add interactive quizzes to your book. |
|
||||
| [mathjax](https://github.com/GitbookIO/plugin-mathjax) | Displays mathematical notation in the book. |
|
||||
| [mixpanel](https://github.com/GitbookIO/plugin-mixpanel) | Mixpanel tracking for your book |
|
||||
| [infinitescroll](https://github.com/GitbookIO/gitbook-plugin-infinitescroll) | Infinite Scrolling |
|
||||
|
||||
##### Other plugins:
|
||||
|
||||
@@ -234,6 +247,8 @@ Plugins can used to extend your book's functionality. Read [GitbookIO/plugin](ht
|
||||
| [Piwik Open Analytics](https://github.com/emmanuel-keller/gitbook-plugin-piwik) | Piwik Open Analytics tracking for your book |
|
||||
| [Heading Anchors](https://github.com/rlmv/gitbook-plugin-anchors) | Add linkable Github-style anchors to headings |
|
||||
| [JSBin](https://github.com/jcouyang/gitbook-plugin-jsbin) | Embedded jsbin frame into your book |
|
||||
| [gitbook-grvis](https://github.com/romanlytkin/gitbook-grvis) | Gitbook GrViz plugin is used to select from markdown dot and converting it into a picture format svg |
|
||||
| [gitbook-plantuml](https://github.com/romanlytkin/gitbook-plantuml) | Gitbook PlantUml plugin is used to select from markdown uml and converting it into a picture format svg |
|
||||
|
||||
#### Debugging
|
||||
|
||||
|
||||
@@ -51,6 +51,11 @@ var makeBuildFunc = function(converter) {
|
||||
|
||||
// Exit process with failure code
|
||||
process.exit(-1);
|
||||
})
|
||||
.then(function() {
|
||||
// Prevent badly behaving plugins
|
||||
// from making the process hang
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
+25
-1
@@ -7,7 +7,7 @@ var prog = require('commander');
|
||||
var tinylr = require('tiny-lr-fork');
|
||||
|
||||
var pkg = require('../package.json');
|
||||
var generators = require("../lib/generate").generators;
|
||||
var genbook = require("../lib/generate");
|
||||
var initDir = require("../lib/generate/init");
|
||||
var fs = require('../lib/generate/fs');
|
||||
|
||||
@@ -80,6 +80,30 @@ build.command(prog.command('serve [source_dir]'))
|
||||
generate();
|
||||
});
|
||||
|
||||
build.commandEbook(prog.command('install [source_dir]'))
|
||||
.description('Install plugins for a book')
|
||||
.action(function(dir, options) {
|
||||
dir = dir || process.cwd();
|
||||
|
||||
console.log("Install plugins in", dir);
|
||||
genbook.config.read({
|
||||
input: dir
|
||||
})
|
||||
.then(function(options) {
|
||||
return genbook.Plugin.install(options);
|
||||
})
|
||||
.then(function() {
|
||||
console.log("Successfully installed plugins!");
|
||||
})
|
||||
.fail(function(err) {
|
||||
// Log error
|
||||
utils.logError(err);
|
||||
|
||||
// Exit process with failure code
|
||||
process.exit(-1);
|
||||
});
|
||||
});
|
||||
|
||||
build.commandEbook(prog.command('pdf [source_dir]'))
|
||||
.description('Build a gitbook as a PDF')
|
||||
.action(function(dir, options) {
|
||||
|
||||
+20724
File diff suppressed because one or more lines are too long
Vendored
+10
File diff suppressed because one or more lines are too long
+38
-5
@@ -1,3 +1,4 @@
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
@@ -16,9 +17,10 @@ var CONFIG = {
|
||||
// Configuration file to use
|
||||
"configFile": "book",
|
||||
|
||||
// Book title and description (defaults are extracted from the README)
|
||||
// Book metadats (somes are extracted from the README by default)
|
||||
"title": null,
|
||||
"description": null,
|
||||
"isbn": null,
|
||||
|
||||
// For ebook format, the extension to use for generation (default is detected from output extension)
|
||||
// "epub", "pdf", "mobi"
|
||||
@@ -38,6 +40,9 @@ var CONFIG = {
|
||||
}
|
||||
},
|
||||
|
||||
// Variables for templating
|
||||
"variables": {},
|
||||
|
||||
// Set another theme with your own layout
|
||||
// It's recommended to use plugins or add more options for default theme, though
|
||||
// See https://github.com/GitbookIO/gitbook/issues/209
|
||||
@@ -83,14 +88,42 @@ var CONFIG = {
|
||||
"left": 62,
|
||||
"top": 36,
|
||||
"bottom": 36
|
||||
}
|
||||
},
|
||||
|
||||
//Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
||||
"headerTemplate": "",
|
||||
|
||||
//Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
||||
"footerTemplate": ""
|
||||
}
|
||||
};
|
||||
|
||||
// Return complete configuration
|
||||
var defaultsConfig = function(options) {
|
||||
return _.merge(options || {}, CONFIG, _.defaults);
|
||||
};
|
||||
|
||||
// Read configuration from book.json
|
||||
var readConfig = function(options) {
|
||||
options = defaultsConfig(options);
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
try {
|
||||
var _config = require(path.resolve(options.input, options.configFile));
|
||||
options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins', 'generator'));
|
||||
}
|
||||
catch(err) {
|
||||
// No config file: not a big deal
|
||||
return Q();
|
||||
}
|
||||
})
|
||||
.thenResolve(options);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
CONFIG: CONFIG,
|
||||
defaults: function(options) {
|
||||
return _.merge(options || {}, CONFIG, _.defaults);
|
||||
}
|
||||
defaults: defaultsConfig,
|
||||
read: readConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,6 @@ var parse = require("../../parse");
|
||||
var BaseGenerator = require("../page");
|
||||
var stringUtils = require("../../utils/string");
|
||||
|
||||
/*
|
||||
* This generator inherits from the single page generator
|
||||
* and convert the page output to ebook
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
};
|
||||
@@ -34,13 +30,18 @@ Generator.prototype.finish = function() {
|
||||
"--cover": that.options.cover,
|
||||
"--title": that.options.title,
|
||||
"--comments": that.options.description,
|
||||
"--isbn": that.options.isbn,
|
||||
"--authors": that.options.author,
|
||||
"--publisher": "GitBook",
|
||||
"--chapter": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter ')]",
|
||||
"--chapter-mark": "pagebreak",
|
||||
"--page-breaks-before": "/",
|
||||
"--level1-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-1 ')]",
|
||||
"--level2-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-2 ')]",
|
||||
"--level3-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-3 ')]",
|
||||
"--no-chapters-in-toc": true
|
||||
"--no-chapters-in-toc": true,
|
||||
"--max-levels": "1",
|
||||
"--breadth-first": true
|
||||
};
|
||||
|
||||
if (format == "pdf") {
|
||||
@@ -51,17 +52,18 @@ Generator.prototype.finish = function() {
|
||||
"--margin-right": String(pdfOptions.margin.right),
|
||||
"--margin-top": String(pdfOptions.margin.top),
|
||||
"--margin-bottom": String(pdfOptions.margin.bottom),
|
||||
"--pdf-add-toc": Boolean(pdfOptions.toc),
|
||||
"--pdf-default-font-size": String(pdfOptions.fontSize),
|
||||
"--pdf-mono-font-size": String(pdfOptions.fontSize),
|
||||
"--paper-size": String(pdfOptions.paperSize),
|
||||
"--pdf-page-numbers": Boolean(pdfOptions.pageNumbers)
|
||||
"--pdf-page-numbers": Boolean(pdfOptions.pageNumbers),
|
||||
"--pdf-header-template": String(pdfOptions.headerTemplate),
|
||||
"--pdf-footer-template": String(pdfOptions.footerTemplate)
|
||||
});
|
||||
}
|
||||
|
||||
var command = [
|
||||
"ebook-convert",
|
||||
path.join(that.options.output, "index.html"),
|
||||
path.join(that.options.output, "SUMMARY.html"),
|
||||
path.join(that.options.output, "index."+format),
|
||||
stringUtils.optionsToShellArgs(_options)
|
||||
].join(" ");
|
||||
|
||||
+15
-1
@@ -64,7 +64,20 @@ var getFiles = function(path) {
|
||||
module.exports = {
|
||||
list: getFiles,
|
||||
readFile: Q.denodeify(fs.readFile),
|
||||
writeFile: Q.denodeify(fs.writeFile),
|
||||
//writeFile: Q.denodeify(fs.writeFile),
|
||||
writeFile: function(filename, data, options) {
|
||||
var d = Q.defer();
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filename, data, options)
|
||||
} catch(err) {
|
||||
d.reject(err);
|
||||
}
|
||||
d.resolve();
|
||||
|
||||
|
||||
return d.promise;
|
||||
},
|
||||
mkdirp: Q.denodeify(fsExtra.mkdirp),
|
||||
copy: Q.denodeify(fsExtra.copy),
|
||||
remove: Q.denodeify(fsExtra.remove),
|
||||
@@ -74,4 +87,5 @@ module.exports = {
|
||||
fs.exists(path, d.resolve);
|
||||
return d.promise;
|
||||
},
|
||||
readFileSync: fs.readFileSync.bind(fs)
|
||||
};
|
||||
|
||||
@@ -8,6 +8,9 @@ var Plugin = require("./plugin");
|
||||
var BaseGenerator = function(options) {
|
||||
this.options = options;
|
||||
|
||||
// Base for assets in plugins
|
||||
this.pluginAssetsBase = "book";
|
||||
|
||||
this.options.plugins = Plugin.normalizeNames(this.options.plugins);
|
||||
this.options.plugins = _.union(this.options.plugins, this.options.defaultsPlugins);
|
||||
this.plugins = [];
|
||||
@@ -17,10 +20,17 @@ BaseGenerator.prototype.callHook = function(name, data) {
|
||||
return this.plugins.hook(name, data);
|
||||
};
|
||||
|
||||
// Sets up generator
|
||||
BaseGenerator.prototype.load = function() {
|
||||
return this.loadPlugins();
|
||||
};
|
||||
|
||||
BaseGenerator.prototype.loadPlugins = function() {
|
||||
var that = this;
|
||||
|
||||
return Plugin.fromList(this.options.plugins, this.options.input, this)
|
||||
return Plugin.fromList(this.options.plugins, this.options.input, this, {
|
||||
assetsBase: this.pluginAssetsBase
|
||||
})
|
||||
.then(function(_plugins) {
|
||||
that.plugins = _plugins;
|
||||
|
||||
@@ -74,6 +84,4 @@ BaseGenerator.prototype.finish = function() {
|
||||
return Q.reject(new Error("Could not finish generation"));
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = BaseGenerator;
|
||||
module.exports = BaseGenerator;
|
||||
|
||||
+17
-21
@@ -40,7 +40,7 @@ var loadGenerator = function(options) {
|
||||
.then(function() {
|
||||
var generator = new generators[options.generator](options);
|
||||
|
||||
return generator.loadPlugins()
|
||||
return generator.load()
|
||||
.then(_.constant(generator));
|
||||
});
|
||||
};
|
||||
@@ -48,28 +48,18 @@ var loadGenerator = function(options) {
|
||||
|
||||
|
||||
var generate = function(options) {
|
||||
// Set defaults to options
|
||||
options = defaultConfig.defaults(options);
|
||||
|
||||
// Validate options
|
||||
if (!options.input) {
|
||||
return Q.reject(new Error("Need option input (book input directory)"));
|
||||
}
|
||||
|
||||
// Check files to get folder type (book, multilanguage book or neither)
|
||||
return checkGenerator(options)
|
||||
|
||||
// Read config file
|
||||
.then(function() {
|
||||
try {
|
||||
var _config = require(path.resolve(options.input, options.configFile));
|
||||
return defaultConfig.read(options)
|
||||
.then(function(_options) {
|
||||
options = _options;
|
||||
|
||||
options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins'));
|
||||
}
|
||||
catch(err) {
|
||||
// No config file: not a big deal
|
||||
return Q();
|
||||
// Validate options
|
||||
if (!options.input) {
|
||||
return Q.reject(new Error("Need option input (book input directory)"));
|
||||
}
|
||||
|
||||
// Check files to get folder type (book, multilanguage book or neither)
|
||||
return checkGenerator(options);
|
||||
})
|
||||
|
||||
// Read readme
|
||||
@@ -99,7 +89,9 @@ var generate = function(options) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Generate a multilanguage book by generating a book for each folder.
|
||||
*/
|
||||
var generateMultiLang = function(options) {
|
||||
var langsSummary;
|
||||
options.output = options.output || path.join(options.input, "_book");
|
||||
@@ -297,6 +289,9 @@ var generateBook = function(options) {
|
||||
})
|
||||
|
||||
// Finish generation
|
||||
.then(function() {
|
||||
return generator.callHook("finish:before");
|
||||
})
|
||||
.then(function() {
|
||||
return generator.finish();
|
||||
})
|
||||
@@ -375,4 +370,5 @@ module.exports = {
|
||||
file: generateFile,
|
||||
book: generateBook,
|
||||
Plugin: Plugin,
|
||||
config: defaultConfig
|
||||
};
|
||||
|
||||
@@ -26,12 +26,12 @@ Generator.prototype.convertFile = function(content, 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(parsed) {
|
||||
json.lexed = parsed.lexed;
|
||||
json.sections = parsed.sections;
|
||||
})
|
||||
.then(function() {
|
||||
return fs.writeFile(
|
||||
@@ -74,4 +74,4 @@ Generator.prototype.finish = function() {
|
||||
// ignore
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
module.exports = Generator;
|
||||
|
||||
+34
-45
@@ -8,52 +8,39 @@ var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../site");
|
||||
|
||||
|
||||
/*
|
||||
* This generator will generate a simple index.html which can be converted as a PDF
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Base for assets in plugins
|
||||
this.pluginAssetsBase = "ebook";
|
||||
|
||||
// List of pages content
|
||||
this.pages = {};
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
// Load all templates
|
||||
Generator.prototype.loadTemplates = function() {
|
||||
this.template = swig.compileFile(
|
||||
this.plugins.template("page") || path.resolve(this.options.theme, 'templates/page.html')
|
||||
this.plugins.template("ebook:page") || path.resolve(this.options.theme, 'templates/ebook/page.html')
|
||||
);
|
||||
this.summaryTemplate = swig.compileFile(
|
||||
this.plugins.template("ebook:sumary") || path.resolve(this.options.theme, 'templates/ebook/summary.html')
|
||||
);
|
||||
this.glossaryTemplate = swig.compileFile(
|
||||
this.plugins.template("ebook:glossary") || path.resolve(this.options.theme, 'templates/ebook/glossary.html')
|
||||
);
|
||||
};
|
||||
|
||||
Generator.prototype.convertFile = function(content, input) {
|
||||
// Generate table of contents
|
||||
Generator.prototype.writeToc = function() {
|
||||
var that = this;
|
||||
var json = {
|
||||
path: input,
|
||||
progress: parse.progress(this.options.navigation, input)
|
||||
};
|
||||
var basePath = ".";
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
return parse.page(content, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(input) || '/',
|
||||
outdir: './',
|
||||
singleFile: true
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
json.content = sections;
|
||||
})
|
||||
.then(function() {
|
||||
that.pages[input] = json;
|
||||
});
|
||||
};
|
||||
|
||||
// Generate languages index
|
||||
Generator.prototype.langsIndex = function(langs) {
|
||||
return Q();
|
||||
return this._writeTemplate(this.summaryTemplate, {
|
||||
toc: parse.progress(this.options.navigation, "README.md").chapters,
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, path.join(this.options.output, "SUMMARY.html"));
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
@@ -62,17 +49,17 @@ Generator.prototype.finish = function() {
|
||||
var output = path.join(this.options.output, "index.html");
|
||||
|
||||
var progress = parse.progress(this.options.navigation, "README.md");
|
||||
|
||||
|
||||
return Q()
|
||||
// Generate html
|
||||
.then(function(pages) {
|
||||
return that._writeTemplate(that.template, {
|
||||
pages: that.pages,
|
||||
progress: progress,
|
||||
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, output);
|
||||
// Write table of contents
|
||||
.then(function() {
|
||||
return that.writeToc();
|
||||
})
|
||||
|
||||
// Write glossary
|
||||
.then(function() {
|
||||
return that.writeGlossary();
|
||||
})
|
||||
|
||||
// Copy cover
|
||||
@@ -82,11 +69,13 @@ Generator.prototype.finish = function() {
|
||||
|
||||
// Copy assets
|
||||
.then(function() {
|
||||
return fs.copy(
|
||||
path.join(that.options.theme, "assets"),
|
||||
path.join(that.options.output, "gitbook")
|
||||
);
|
||||
return that.copyAssets();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
// Generate languages index
|
||||
Generator.prototype.langsIndex = function(langs) {
|
||||
return Q();
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
|
||||
+73
-27
@@ -4,11 +4,12 @@ var semver = require("semver");
|
||||
var path = require("path");
|
||||
var url = require("url");
|
||||
var fs = require("./fs");
|
||||
var npmi = require('npmi');
|
||||
var resolve = require('resolve');
|
||||
|
||||
var pkg = require("../../package.json");
|
||||
|
||||
var RESOURCES = ["js", "css"];
|
||||
var RESOURCES = ["js", "css"];
|
||||
|
||||
var Plugin = function(name, root, generator) {
|
||||
this.name = name;
|
||||
@@ -65,8 +66,9 @@ Plugin.prototype.normalizeResource = function(resource) {
|
||||
};
|
||||
|
||||
// Return resources
|
||||
Plugin.prototype._getResources = function() {
|
||||
var book = this.infos.book;
|
||||
Plugin.prototype._getResources = function(base) {
|
||||
base = base || "book";
|
||||
var book = this.infos[base];
|
||||
|
||||
// Nothing specified, fallback to default
|
||||
if (!book) {
|
||||
@@ -84,12 +86,10 @@ Plugin.prototype._getResources = function() {
|
||||
};
|
||||
|
||||
// Normalize resources and return them
|
||||
Plugin.prototype.getResources = function() {
|
||||
Plugin.prototype.getResources = function(base) {
|
||||
var that = this;
|
||||
|
||||
|
||||
|
||||
return this._getResources()
|
||||
return this._getResources(base)
|
||||
.then(function(resources) {
|
||||
|
||||
_.each(RESOURCES, function(resourceType) {
|
||||
@@ -133,10 +133,14 @@ Plugin.prototype.callHook = function(name, data) {
|
||||
};
|
||||
|
||||
// Copy plugin assets fodler
|
||||
Plugin.prototype.copyAssets = function(out) {
|
||||
Plugin.prototype.copyAssets = function(out, options) {
|
||||
var that = this;
|
||||
options = _.defaults(options || {}, {
|
||||
base: "book"
|
||||
});
|
||||
|
||||
return this.getResources().get('assets')
|
||||
return this.getResources(options.base)
|
||||
.get('assets')
|
||||
.then(function(assets) {
|
||||
// Assets are undefined
|
||||
if(!assets) return false;
|
||||
@@ -149,39 +153,81 @@ Plugin.prototype.copyAssets = function(out) {
|
||||
};
|
||||
|
||||
|
||||
// Install a list of plugin
|
||||
Plugin.install = function(options) {
|
||||
// Normalize list of plugins
|
||||
var plugins = Plugin.normalizeList(options.plugins);
|
||||
|
||||
// Install plugins one by one
|
||||
return _.reduce(plugins, function(prev, plugin) {
|
||||
return prev.then(function() {
|
||||
var fullname = "gitbook-plugin-"+plugin.name;
|
||||
console.log("Install plugin", plugin.name, "from npm ("+fullname+") with version", (plugin.version || "*"));
|
||||
return Q.nfcall(npmi, {
|
||||
'name': fullname,
|
||||
'version': plugin.version,
|
||||
'path': options.input,
|
||||
'npmLoad': {
|
||||
'loglevel': 'silent',
|
||||
'loaded': false,
|
||||
'prefix': options.input
|
||||
}
|
||||
});
|
||||
});
|
||||
}, Q());
|
||||
};
|
||||
|
||||
// Normalize a list of plugin name to use
|
||||
Plugin.normalizeNames = function(names) {
|
||||
// Normalize a list of plugins to use
|
||||
Plugin.normalizeList = function(plugins) {
|
||||
// Normalize list to an array
|
||||
names = _.isString(names) ? names.split(",") : (names || []);
|
||||
plugins = _.isString(plugins) ? plugins.split(",") : (plugins || []);
|
||||
|
||||
// Divide as {name, version} to handle format like "myplugin@1.0.0"
|
||||
plugins = _.map(plugins, function(plugin) {
|
||||
var parts = plugin.split("@");
|
||||
return {
|
||||
'name': parts[0],
|
||||
'version': parts[1] // optional
|
||||
}
|
||||
});
|
||||
|
||||
// List plugins to remove
|
||||
var toremove = _.chain(names)
|
||||
.filter(function(name) {
|
||||
return name.length > 0 && name[0] == "-";
|
||||
var toremove = _.chain(plugins)
|
||||
.filter(function(plugin) {
|
||||
return plugin.name.length > 0 && plugin.name[0] == "-";
|
||||
})
|
||||
.map(function(name) {
|
||||
return name.slice(1);
|
||||
.map(function(plugin) {
|
||||
return plugin.name.slice(1);
|
||||
})
|
||||
.value();
|
||||
|
||||
// Merge with defaults
|
||||
names = _.chain(names)
|
||||
.concat(Plugin.defaults)
|
||||
plugins = _.chain(plugins)
|
||||
.concat(_.map(Plugin.defaults, function(plugin) {
|
||||
return { 'name': plugin }
|
||||
}))
|
||||
.uniq()
|
||||
.value();
|
||||
|
||||
// Remove plugins starting with
|
||||
names = _.filter(names, function(name) {
|
||||
return !_.contains(toremove, name) && !(name.length > 0 && name[0] == "-");
|
||||
// Build final list
|
||||
plugins = _.filter(plugins, function(plugin) {
|
||||
return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == "-");
|
||||
});
|
||||
|
||||
return names;
|
||||
return plugins;
|
||||
};
|
||||
|
||||
// Normalize a list of plugin name to use
|
||||
Plugin.normalizeNames = function(plugins) {
|
||||
return _.pluck(Plugin.normalizeList(plugins), "name");
|
||||
};
|
||||
|
||||
// Extract data from a list of plugin
|
||||
Plugin.fromList = function(names, root, generator) {
|
||||
Plugin.fromList = function(names, root, generator, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
assetsBase: "book"
|
||||
});
|
||||
|
||||
var failed = [];
|
||||
|
||||
// Load plugins
|
||||
@@ -191,14 +237,14 @@ Plugin.fromList = function(names, root, generator) {
|
||||
return plugin;
|
||||
});
|
||||
|
||||
if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")));
|
||||
if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")+". Run 'gitbook install' to install plugins from NPM."));
|
||||
|
||||
// The raw resources extracted from each plugin
|
||||
var pluginResources;
|
||||
|
||||
// Get resources of plugins
|
||||
return Q.all(_.map(plugins, function(plugin) {
|
||||
return plugin.getResources();
|
||||
return plugin.getResources(options.assetsBase);
|
||||
}))
|
||||
// Extract resources out
|
||||
// css, js, etc ...
|
||||
@@ -271,7 +317,7 @@ Plugin.fromList = function(names, root, generator) {
|
||||
});
|
||||
};
|
||||
|
||||
// Default plugins
|
||||
// Default plugins added to each books
|
||||
Plugin.defaults = [
|
||||
"mathjax"
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var _ = require("lodash");
|
||||
|
||||
var kramed = require('kramed');
|
||||
var textRenderer = require('kramed-text-renderer');
|
||||
|
||||
|
||||
+63
-44
@@ -23,29 +23,29 @@ var Generator = function() {
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
// Load all templates
|
||||
Generator.prototype.loadTemplates = function() {
|
||||
this.template = swig.compileFile(
|
||||
this.plugins.template("site") || path.resolve(this.options.theme, 'templates/site.html')
|
||||
);
|
||||
this.langsTemplate = swig.compileFile(
|
||||
this.plugins.template("langs") || path.resolve(this.options.theme, 'templates/langs.html')
|
||||
);
|
||||
this.glossaryTemplate = swig.compileFile(
|
||||
this.plugins.template("glossary") || path.resolve(this.options.theme, 'templates/glossary.html')
|
||||
);
|
||||
};
|
||||
|
||||
// Load plugins
|
||||
Generator.prototype.loadPlugins = function() {
|
||||
// Add template loading to load
|
||||
Generator.prototype.load = function() {
|
||||
var that = this;
|
||||
|
||||
return BaseGenerator.prototype.loadPlugins.apply(this)
|
||||
return BaseGenerator.prototype.load.apply(this)
|
||||
.then(function() {
|
||||
return that.loadTemplates();
|
||||
});
|
||||
};
|
||||
|
||||
// Load all templates
|
||||
Generator.prototype.loadTemplates = function() {
|
||||
this.template = swig.compileFile(
|
||||
this.plugins.template("site:page") || path.resolve(this.options.theme, 'templates/book/page.html')
|
||||
);
|
||||
this.langsTemplate = swig.compileFile(
|
||||
this.plugins.template("site:langs") || path.resolve(this.options.theme, 'templates/book/langs.html')
|
||||
);
|
||||
this.glossaryTemplate = swig.compileFile(
|
||||
this.plugins.template("site:glossary") || path.resolve(this.options.theme, 'templates/book/glossary.html')
|
||||
);
|
||||
};
|
||||
|
||||
// Generate a template
|
||||
Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
|
||||
var that = this;
|
||||
@@ -59,10 +59,6 @@ Generator.prototype._writeTemplate = function(tpl, options, output, interpolate)
|
||||
title: that.options.title,
|
||||
description: that.options.description,
|
||||
|
||||
githubAuthor: that.options.github ? that.options.github.split("/")[0] : "",
|
||||
githubId: that.options.github,
|
||||
githubHost: that.options.githubHost,
|
||||
|
||||
glossary: that.options.glossary,
|
||||
|
||||
summary: that.options.summary,
|
||||
@@ -95,20 +91,15 @@ Generator.prototype.indexPage = function(lexed, pagePath) {
|
||||
return Q();
|
||||
};
|
||||
|
||||
// Convert a markdown file to html
|
||||
Generator.prototype.convertFile = function(content, _input) {
|
||||
// Convert a markdown file into a normalized data set
|
||||
Generator.prototype.prepareFile = function(content, _input) {
|
||||
var that = this;
|
||||
|
||||
_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) || ".";
|
||||
|
||||
var page = {
|
||||
path: _input,
|
||||
rawPath: input, // path to raw md file
|
||||
rawPath: input,
|
||||
content: content,
|
||||
progress: parse.progress(this.options.navigation, _input)
|
||||
};
|
||||
@@ -127,33 +118,59 @@ Generator.prototype.convertFile = function(content, _input) {
|
||||
return _callHook("page:before");
|
||||
})
|
||||
.then(function() {
|
||||
// Lex page
|
||||
return parse.lex(page.content);
|
||||
})
|
||||
.then(function(lexed) {
|
||||
// Index page in search
|
||||
return that.indexPage(lexed, _output)
|
||||
.then(_.constant(lexed));
|
||||
})
|
||||
.then(function(lexed) {
|
||||
// Lex, parse includes and get
|
||||
// Get HTML generated sections
|
||||
return parse.page(lexed, {
|
||||
repo: that.options.githubId,
|
||||
return parse.page(page.content, {
|
||||
// Local files path
|
||||
dir: path.dirname(_input) || '/',
|
||||
|
||||
// Output directory
|
||||
outdir: path.dirname(_input) || '/',
|
||||
|
||||
// Includer for templating
|
||||
includer: parse.includer(that.options.variables, [
|
||||
path.dirname(_input) || '/',
|
||||
path.join(that.options.input, '_includes'),
|
||||
], path.join, fs.readFileSync)
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
page.sections = sections;
|
||||
.then(function(parsed) {
|
||||
page.lexed = parsed.lexed;
|
||||
page.sections = parsed.sections;
|
||||
|
||||
// Use plugin hook
|
||||
return _callHook("page");
|
||||
})
|
||||
.then(function() {
|
||||
return page;
|
||||
});
|
||||
};
|
||||
|
||||
// Convert a markdown file to html
|
||||
Generator.prototype.convertFile = function(content, _input) {
|
||||
var that = this;
|
||||
|
||||
var _output = _input.replace(".md", ".html");
|
||||
if (_output == "README.html") _output = "index.html";
|
||||
var output = path.join(this.options.output, _output);
|
||||
var basePath = path.relative(path.dirname(output), this.options.output) || ".";
|
||||
|
||||
// Bug fix for issue #493 which would occur when relative-links are 2-level or more deep in win32
|
||||
if (process.platform === 'win32') {
|
||||
basePath = basePath.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
return this.prepareFile(content, _input)
|
||||
.then(function(page) {
|
||||
// Index page in search
|
||||
return that.indexPage(page.lexed, _output).thenResolve(page);
|
||||
})
|
||||
.then(function(page) {
|
||||
// Write file
|
||||
return that._writeTemplate(that.template, {
|
||||
progress: page.progress,
|
||||
|
||||
_input: _input,
|
||||
_input: page.path,
|
||||
content: page.sections,
|
||||
|
||||
basePath: basePath,
|
||||
@@ -161,7 +178,7 @@ Generator.prototype.convertFile = function(content, _input) {
|
||||
}, output, function(html) {
|
||||
page.content = html;
|
||||
|
||||
return _callHook("page:after").get("content")
|
||||
return that.callHook("page:after", page).get("content")
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -238,7 +255,9 @@ Generator.prototype.copyAssets = function() {
|
||||
return Q.all(
|
||||
_.map(that.plugins.list, function(plugin) {
|
||||
var pluginAssets = path.join(that.options.output, "gitbook/plugins/", plugin.name);
|
||||
return plugin.copyAssets(pluginAssets);
|
||||
return plugin.copyAssets(pluginAssets, {
|
||||
base: that.pluginAssetsBase
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
var fs = require('graceful-fs');
|
||||
var path = require('path');
|
||||
|
||||
module.exports = function(code, folder) {
|
||||
folder = folder || '';
|
||||
|
||||
return code.replace(/{{([\s\S]+?)}}/g, function(match, filename) {
|
||||
// Normalize filename
|
||||
var fname = path.join(folder, filename.trim());
|
||||
|
||||
// Try including snippet from FS
|
||||
try {
|
||||
// Trim trailing newlines/space of imported snippets
|
||||
return fs.readFileSync(fname, 'utf8').trimRight();
|
||||
} catch(err) {}
|
||||
|
||||
// If fails leave content as is
|
||||
return match;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
module.exports = function(markdown, includer) {
|
||||
// Memoized include function (to cache lookups)
|
||||
var _include = _.memoize(includer);
|
||||
|
||||
return markdown.replace(/{{([\s\S]+?)}}/g, function(match, key) {
|
||||
// If fails leave content as is
|
||||
key = key.trim();
|
||||
return _include(key) || match;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Return a fs inclduer
|
||||
module.exports = function(ctx, folders, resolveFile, readFile) {
|
||||
return function(name) {
|
||||
return ctx[name] ||
|
||||
folders.map(function(folder) {
|
||||
// Try including snippet from FS
|
||||
try {
|
||||
var fname = resolveFile(folder, name);
|
||||
// Trim trailing newlines/space of imported snippets
|
||||
return readFile(fname, 'utf8').trimRight();
|
||||
} catch(err) {}
|
||||
})
|
||||
.filter(Boolean)[0];
|
||||
}
|
||||
};
|
||||
+2
-1
@@ -6,5 +6,6 @@ module.exports = {
|
||||
lex: require('./lex'),
|
||||
progress: require('./progress'),
|
||||
navigation: require('./navigation'),
|
||||
readme: require('./readme')
|
||||
readme: require('./readme'),
|
||||
includer: require('./includer')
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
function isExercise(nodes) {
|
||||
var codeType = { type: 'code' };
|
||||
|
||||
// Number of code nodes in section
|
||||
var len = _.filter(nodes, codeType).length;
|
||||
|
||||
return (
|
||||
// Got 3 or 4 code blocks
|
||||
(len === 3 || len === 4) &&
|
||||
// Ensure all nodes are at the end
|
||||
_.all(_.last(nodes, len), codeType)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = isExercise;
|
||||
@@ -0,0 +1,87 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
function isQuizNode(node) {
|
||||
return (/^[(\[][ x][)\]]/).test(node.text || node);
|
||||
}
|
||||
|
||||
function isTableQuestion(nodes) {
|
||||
var block = questionBlock(nodes);
|
||||
return (
|
||||
block.length === 1 &&
|
||||
block[0].type === 'table' &&
|
||||
_.all(block[0].cells[0].slice(1), isQuizNode)
|
||||
);
|
||||
}
|
||||
|
||||
function isListQuestion(nodes) {
|
||||
var block = questionBlock(nodes);
|
||||
// Counter of when we go in and out of lists
|
||||
var inlist = 0;
|
||||
// Number of lists we found
|
||||
var lists = 0;
|
||||
// Elements found outside a list
|
||||
var outsiders = 0;
|
||||
// Ensure that we have nothing except lists
|
||||
_.each(block, function(node) {
|
||||
if(node.type === 'list_start') {
|
||||
inlist++;
|
||||
} else if(node.type === 'list_end') {
|
||||
inlist--;
|
||||
lists++;
|
||||
} else if(inlist === 0) {
|
||||
// Found non list_start or list_end whilst outside a list
|
||||
outsiders++;
|
||||
}
|
||||
});
|
||||
return lists > 0 && outsiders === 0;
|
||||
}
|
||||
|
||||
function isQuestion(nodes) {
|
||||
return isListQuestion(nodes) || isTableQuestion(nodes);
|
||||
}
|
||||
|
||||
// Remove (optional) paragraph header node and blockquote
|
||||
function questionBlock(nodes) {
|
||||
return nodes.slice(
|
||||
nodes[0].type === 'paragraph' ? 1 : 0,
|
||||
_.findIndex(nodes, { type: 'blockquote_start' })
|
||||
);
|
||||
}
|
||||
|
||||
function splitQuestions(nodes) {
|
||||
// Represents nodes in current question
|
||||
var buffer = [];
|
||||
return _.reduce(nodes, function(accu, node) {
|
||||
// Add node to buffer
|
||||
buffer.push(node);
|
||||
|
||||
// Flush buffer once we hit the end of a question
|
||||
if(node.type === 'blockquote_end') {
|
||||
accu.push(buffer);
|
||||
// Clear buffer
|
||||
buffer = [];
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function isQuiz(nodes) {
|
||||
// Extract potential questions
|
||||
var questions = splitQuestions(
|
||||
// Skip quiz title if there
|
||||
nodes.slice(
|
||||
(nodes[0] && nodes[0].type) === 'paragraph' ? 1 : 0
|
||||
)
|
||||
);
|
||||
|
||||
// Nothing that looks like questions
|
||||
if(questions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure all questions are correctly structured
|
||||
return _.all(questions, isQuestion);
|
||||
}
|
||||
|
||||
module.exports = isQuiz;
|
||||
+3
-53
@@ -1,6 +1,9 @@
|
||||
var _ = require('lodash');
|
||||
var kramed = require('kramed');
|
||||
|
||||
var isExercise = require('./is_exercise');
|
||||
var isQuiz = require('./is_quiz');
|
||||
|
||||
// Split a page up into sections (lesson, exercises, ...)
|
||||
function splitSections(nodes) {
|
||||
var section = [];
|
||||
@@ -17,59 +20,6 @@ function splitSections(nodes) {
|
||||
}, []).concat([section]); // Add remaining nodes
|
||||
}
|
||||
|
||||
function isQuizNode(node) {
|
||||
return (/^[(\[][ x][)\]]/).test(node.text || node);
|
||||
}
|
||||
|
||||
function isExercise(nodes) {
|
||||
var codeType = { type: 'code' };
|
||||
|
||||
// Number of code nodes in section
|
||||
var len = _.filter(nodes, codeType).length;
|
||||
|
||||
return (
|
||||
// Got 3 or 4 code blocks
|
||||
(len === 3 || len === 4) &&
|
||||
// Ensure all nodes are at the end
|
||||
_.all(_.last(nodes, len), codeType)
|
||||
);
|
||||
}
|
||||
|
||||
function isQuiz(nodes) {
|
||||
if (nodes.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Support having a first paragraph block
|
||||
// before our series of questions
|
||||
var quizNodes = nodes.slice(nodes[0].type === 'paragraph' ? 1 : 0);
|
||||
|
||||
// No questions
|
||||
if (!_.some(quizNodes, { type: 'blockquote_start' })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if section has list of questions
|
||||
// or table of questions
|
||||
var listIdx = _.findIndex(quizNodes, { type: 'list_item_start' });
|
||||
var tableIdx = _.findIndex(quizNodes, { type: 'table' });
|
||||
|
||||
if(
|
||||
// List of questions
|
||||
listIdx !== -1 && isQuizNode(quizNodes[listIdx + 1]) ||
|
||||
|
||||
// Table of questions
|
||||
(
|
||||
tableIdx !== -1 &&
|
||||
_.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// What is the type of this section
|
||||
function sectionType(nodes, idx) {
|
||||
if(isExercise(nodes)) {
|
||||
|
||||
+10
-11
@@ -5,7 +5,7 @@ var hljs = require('highlight.js');
|
||||
var lex = require('./lex');
|
||||
var renderer = require('./renderer');
|
||||
|
||||
var codeInclude = require('./code_include');
|
||||
var include = require('./include');
|
||||
var lnormalize = require('../utils/lang').normalize;
|
||||
|
||||
|
||||
@@ -51,8 +51,10 @@ function parsePage(src, options) {
|
||||
options = options || {};
|
||||
|
||||
// Lex if not already lexed
|
||||
return (_.isArray(src) ? src : lex(src))
|
||||
.map(function(section) {
|
||||
var parsed = {
|
||||
lexed: (_.isArray(src) ? src : lex(include(src, options.includer || function() { return undefined; })))
|
||||
};
|
||||
parsed.sections = parsed.lexed.map(function(section) {
|
||||
// Transform given type
|
||||
if(section.type === 'exercise') {
|
||||
var nonCodeNodes = _.reject(section, {
|
||||
@@ -74,20 +76,15 @@ function parsePage(src, options) {
|
||||
// Main language
|
||||
var lang = validLangs ? langs[0] : null;
|
||||
|
||||
// codeInclude shortcut
|
||||
var ci = function(code) {
|
||||
return codeInclude(code, options.dir);
|
||||
};
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
type: section.type,
|
||||
content: render(nonCodeNodes, options),
|
||||
lang: lang,
|
||||
code: {
|
||||
base: ci(codeNodes[0].text),
|
||||
solution: ci(codeNodes[1].text),
|
||||
validation: ci(codeNodes[2].text),
|
||||
base: codeNodes[0].text,
|
||||
solution: codeNodes[1].text,
|
||||
validation: codeNodes[2].text,
|
||||
// Context is optional
|
||||
context: codeNodes[3] ? codeNodes[3].text : null,
|
||||
}
|
||||
@@ -155,6 +152,8 @@ function parsePage(src, options) {
|
||||
content: render(section, options)
|
||||
};
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Exports
|
||||
|
||||
+10
-24
@@ -1,11 +1,7 @@
|
||||
var url = require('url');
|
||||
var _ = require('lodash');
|
||||
var inherits = require('util').inherits;
|
||||
var links = require('../utils').links;
|
||||
var codeInclude = require('./code_include');
|
||||
|
||||
|
||||
var path = require('path');
|
||||
|
||||
var kramed = require('kramed');
|
||||
|
||||
var rendererId = 0;
|
||||
@@ -52,24 +48,13 @@ GitBookRenderer.prototype.link = function(href, title, text) {
|
||||
|
||||
// Parsed version of the url
|
||||
var parsed = url.parse(href);
|
||||
|
||||
var o = this._extra_options;
|
||||
var extname = parsed.path? _.last(parsed.path.split(".")) : "";
|
||||
|
||||
// Relative link, rewrite it to point to github repo
|
||||
if(links.isRelative(_href)) {
|
||||
if (path.extname(parsed.path) == ".md") {
|
||||
_href = links.toAbsolute(_href, o.dir || "./", o.outdir || "./");
|
||||
|
||||
if (o.singleFile) {
|
||||
_href = "#"+_href;
|
||||
} else {
|
||||
_href = _href.replace(".md", ".html");
|
||||
}
|
||||
} else if (o && o.repo && o.dir) {
|
||||
href = url.resolve('https://github.com/' + o.repo + '/blob/', [o.dir, _href].join("/"));
|
||||
parsed = url.parse(href);
|
||||
_href = parsed.href;
|
||||
}
|
||||
if(links.isRelative(_href) && extname == "md") {
|
||||
_href = links.toAbsolute(_href, o.dir || "./", o.outdir || "./");
|
||||
_href = _href.replace(".md", ".html");
|
||||
}
|
||||
|
||||
// Generate HTML for link
|
||||
@@ -118,8 +103,10 @@ GitBookRenderer.prototype._createCheckboxAndRadios = function(text) {
|
||||
if (!match) {
|
||||
return text;
|
||||
}
|
||||
var quizIdentifier = 'quiz-row-' + this.id + '-' + this.quizRowId + '-' + this.quizIndex++;
|
||||
var field = "<input name='" + quizIdentifier + "' id='" + quizIdentifier + "' type='";
|
||||
//fix radio input uncheck failed
|
||||
var quizFieldName='quiz-row-' + this.id + '-' + this.quizRowId ;
|
||||
var quizIdentifier = quizFieldName + '-' + this.quizIndex++;
|
||||
var field = "<input name='" + quizFieldName + "' id='" + quizIdentifier + "' type='";
|
||||
field += match[1] === '(' ? "radio" : "checkbox";
|
||||
field += match[2] === 'x' ? "' checked/>" : "'/>";
|
||||
var splittedText = text.split(fieldRegex);
|
||||
@@ -139,8 +126,7 @@ GitBookRenderer.prototype.listitem = function(text) {
|
||||
GitBookRenderer.prototype.code = function(code, lang, escaped) {
|
||||
return GitBookRenderer.super_.prototype.code.call(
|
||||
this,
|
||||
// Import code snippets
|
||||
codeInclude(code, this._extra_options.dir),
|
||||
code,
|
||||
lang,
|
||||
escaped
|
||||
);
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ function escapeShellArg(arg) {
|
||||
function optionsToShellArgs(options) {
|
||||
return _.chain(options)
|
||||
.map(function(value, key) {
|
||||
if (value == null || value == false) return null;
|
||||
if (value == true) return key;
|
||||
if (value == null || value === false) return null;
|
||||
if (value === true) return key;
|
||||
return key+"="+escapeShellArg(value);
|
||||
})
|
||||
.compact()
|
||||
|
||||
+11
-6
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "1.0.0",
|
||||
"version": "1.4.2",
|
||||
"homepage": "http://www.gitbook.io/",
|
||||
"description": "Library and cmd utility to generate GitBooks",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"q": "1.0.1",
|
||||
"lodash": "2.4.1",
|
||||
"kramed": "0.4.1",
|
||||
"kramed": "0.4.3",
|
||||
"kramed-text-renderer": "0.2.1",
|
||||
"lunr": "0.5.2",
|
||||
"swig": "1.3.2",
|
||||
@@ -16,15 +16,18 @@
|
||||
"commander": "2.2.0",
|
||||
"graceful-fs": "3.0.2",
|
||||
"fs-extra": "0.10.0",
|
||||
"highlight.js": "8.1.0",
|
||||
"highlight.js": "8.3.0",
|
||||
"tmp": "0.0.23",
|
||||
"semver": "2.2.1",
|
||||
"npmi": "0.1.1",
|
||||
"gaze": "~0.5.1",
|
||||
"resolve": "0.6.3",
|
||||
"tiny-lr-fork": "0.0.5",
|
||||
"gitbook-plugin": "0.0.2",
|
||||
"gitbook-plugin-mathjax": "0.0.4",
|
||||
"gitbook-plugin-livereload": "0.0.1"
|
||||
"gitbook-plugin-mathjax": "0.0.6",
|
||||
"gitbook-plugin-livereload": "0.0.1",
|
||||
"gitbook-plugin-exercises": "1.0.0",
|
||||
"gitbook-plugin-quizzes": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.18.2",
|
||||
@@ -33,7 +36,9 @@
|
||||
"grunt-contrib-copy": "0.5.0",
|
||||
"grunt-contrib-less": "~0.5.0",
|
||||
"grunt-contrib-requirejs": "0.4.1",
|
||||
"grunt-bower-install-simple": "0.9.2"
|
||||
"grunt-bower-install-simple": "0.9.2",
|
||||
"grunt-browserify": "3.1.0",
|
||||
"grunt-contrib-uglify": "0.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "export TESTING=true; mocha --reporter list"
|
||||
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
# Learn Node.js by Example
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- [x] A computer with internet access
|
||||
- [ ] Time: 30h (e.g. 2 months 30 mins per day or **1 week intensive**)
|
||||
|
||||
|
||||
## What is Node.js ?
|
||||
|
||||
Node.js lets you *easily* build networked software (websites, applications "apps",
|
||||
using JavaScript).
|
||||
|
||||
Its not "*point-and-click*" like WordPress, SquareSpace or Salesforce;
|
||||
you will need to write some "code". But as I will demonstrate, that's
|
||||
a *lot* easier than it sounds and gives you more power/flexibility
|
||||
and puts you in full control.
|
||||
|
||||
## Node.js is not "Version 1.0" yet can I used it in Production?
|
||||
|
||||
Yes! Some of the biggest organisations/companies in the world
|
||||
are using Node.js in Production systems:
|
||||
|
||||
[Alibaba](https://github.com/alibaba/node-hbase-client),
|
||||
[Ajax.org](Ajax.org),
|
||||
[Box.com](http://tech.blog.box.com/2014/06/node-js-high-availability-at-box/), British Sky Broadcasting (Sky/Now TV),
|
||||
CNN,
|
||||
[Cloudup](https://cloudup.com/),
|
||||
Conde Nast,
|
||||
[DirectTV](http://strongloop.com/strongblog/node-summit-media-companies-embrace-node-js-for-rapidly-developing-responsive-apps/),
|
||||
[Dow Jones](http://nodejs.org/industry),
|
||||
eBay,
|
||||
[FeedHenry](http://www.feedhenry.com/),
|
||||
[GitHub](https://twitter.com/github/status/16979699217465344),
|
||||
[Google](http://venturebeat.com/2012/01/24/node-at-google-mozilla-yahoo/),
|
||||
[Groupon](http://nodeup.com/fiftyeight),
|
||||
HBO,
|
||||
Help.com,
|
||||
[HP](https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node),
|
||||
iTV,
|
||||
[Joyent](https://www.joyent.com/) (duh!),
|
||||
[Klout](https://klout.com),
|
||||
LinkedIn,
|
||||
McDonalds,
|
||||
[Medium](https://medium.com/the-story),
|
||||
Mozilla,
|
||||
NetFlix,
|
||||
[OpenTable](http://hapijs.com/community),
|
||||
PayPal,
|
||||
Pearson,
|
||||
~~Q~~,
|
||||
[Revolt](http://revolt.tv/),
|
||||
[Square](https://modulus.io/companies-using-node),
|
||||
Tesco,
|
||||
ThomasCook,
|
||||
Trello,
|
||||
Uber,
|
||||
Voxer,
|
||||
Walmart,
|
||||
Wikimedia (in progress of moving to SOA with node!)
|
||||
Yahoo,
|
||||
Yammer,
|
||||
[Yandex](https://www.youtube.com/watch?v=zdCxgdH4wZo),
|
||||
[Zendesk](http://radar.zendesk.com/)
|
||||
|
||||
Want more? See: http://nodejs.org/industry/ and <br />
|
||||
https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node
|
||||
|
||||
|
||||
# Try it!
|
||||
|
||||
## Download & Install
|
||||
|
||||
> http://nodejs.org/download/
|
||||
|
||||
|
||||
## Node.js (Core) API
|
||||
|
||||
The node.js ("core") has many useful modules.
|
||||
|
||||
Bookmark: [http://nodejs.org/api](http://nodejs.org/api/) (you will come back to it)
|
||||
|
||||
|
||||
|
||||
## Stability (Can we use it?)
|
||||
|
||||
> *Which node.js* ***core*** *package(s) can/should I use?*
|
||||
|
||||
Every core module has a
|
||||
["***Stability Index***"](http://nodejs.org/api/documentation.html#documentation_stability_index)
|
||||
rating on the node.js API.
|
||||
|
||||
**General rule**: If you are being *paid* to write code
|
||||
that runs in node.js, <br /> pick core modules/methods
|
||||
with stability **Stable**, **API Frozen** and **Locked**.
|
||||
|
||||

|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
- [**cluster**](http://nodejs.org/api/cluster.html) is ***Experimental*** - don't use
|
||||
- [**domain**](http://nodejs.org/api/domain.html) is ***Unstable*** - don't use
|
||||
- [**path**](http://nodejs.org/api/path.html) is ***Stable*** - use
|
||||
- [**events**](http://nodejs.org/api/events.html) is ***Frozen*** - use
|
||||
- [**assert**](http://nodejs.org/api/assert.html) is ***Locked*** - use
|
||||
|
||||
Core Modules to Learn
|
||||
|
||||
- path
|
||||
- os
|
||||
|
||||
|
||||
|
||||
Community Modules to Learn:
|
||||
|
||||
- [jscs](https://www.npmjs.org/package/jscs) - code style checker
|
||||
- [q](https://www.npmjs.org/package/q) - promises library
|
||||
- [nd](https://www.npmjs.org/package/nd) - view documentation for a module
|
||||
+5
-1
@@ -3,12 +3,13 @@ var path = require('path');
|
||||
var assert = require('assert');
|
||||
|
||||
var page = require('../').parse.page;
|
||||
var includer = require('../').parse.includer;
|
||||
|
||||
var FIXTURES_DIR = path.join(__dirname, './fixtures/');
|
||||
|
||||
function loadPage (name, options) {
|
||||
var CONTENT = fs.readFileSync(FIXTURES_DIR + name + '.md', 'utf8');
|
||||
return page(CONTENT, options);
|
||||
return page(CONTENT, options).sections;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +17,9 @@ describe('Code includes', function() {
|
||||
|
||||
var LEXED = loadPage('INCLUDES', {
|
||||
'dir': FIXTURES_DIR,
|
||||
'includer': includer({}, [
|
||||
FIXTURES_DIR
|
||||
], path.join, fs.readFileSync)
|
||||
});
|
||||
|
||||
var INCLUDED_C = fs.readFileSync(path.join(FIXTURES_DIR, 'included.c'), 'utf8');
|
||||
|
||||
+9
-38
@@ -6,7 +6,7 @@ var page = require('../').parse.page;
|
||||
|
||||
function loadPage (name, options) {
|
||||
var CONTENT = fs.readFileSync(path.join(__dirname, './fixtures/' + name + '.md'), 'utf8');
|
||||
return page(CONTENT, options);
|
||||
return page(CONTENT, options).sections;
|
||||
}
|
||||
|
||||
var LEXED = loadPage('PAGE', {
|
||||
@@ -78,24 +78,8 @@ describe('Page parsing', function() {
|
||||
|
||||
|
||||
describe('Relative links', function() {
|
||||
it('should be resolved to their GitHub counterparts', function() {
|
||||
var LEXED = loadPage('GITHUB_LINKS', {
|
||||
// GitHub repo ID
|
||||
repo: 'GitBookIO/javascript',
|
||||
|
||||
// Imaginary folder of markdown file
|
||||
dir: 'course',
|
||||
outdir: './'
|
||||
});
|
||||
|
||||
assert(LEXED[0].content.indexOf('https://github.com/GitBookIO/javascript/blob/src/something.cpp') !== -1);
|
||||
});
|
||||
|
||||
it('should replace link to .md by link to .html', function() {
|
||||
var LEXED = loadPage('MARKDOWN_LINKS', {
|
||||
// GitHub repo ID
|
||||
repo: 'GitBookIO/javascript',
|
||||
|
||||
// Imaginary folder of markdown file
|
||||
dir: 'course',
|
||||
outdir: 'course'
|
||||
@@ -104,29 +88,11 @@ describe('Relative links', function() {
|
||||
assert(LEXED[0].content.indexOf('test.html') !== -1);
|
||||
assert(LEXED[0].content.indexOf('../before.html') !== -1);
|
||||
});
|
||||
|
||||
it('should replace link to .md by link to page in format single page', function() {
|
||||
var LEXED = loadPage('MARKDOWN_LINKS', {
|
||||
// GitHub repo ID
|
||||
repo: 'GitBookIO/javascript',
|
||||
|
||||
// Imaginary folder of markdown file
|
||||
dir: 'course',
|
||||
outdir: './',
|
||||
singleFile: true
|
||||
});
|
||||
|
||||
assert(LEXED[0].content.indexOf('#course/test.md') !== -1);
|
||||
assert(LEXED[0].content.indexOf('#before.md') !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relative images', function() {
|
||||
it('should keep image relative with considering output directory in site format', function() {
|
||||
var LEXED = loadPage('IMAGES', {
|
||||
// GitHub repo ID
|
||||
repo: 'GitBookIO/javascript',
|
||||
|
||||
// Imaginary folder of markdown file
|
||||
dir: 'syntax',
|
||||
outdir: 'syntax'
|
||||
@@ -138,9 +104,6 @@ describe('Relative images', function() {
|
||||
|
||||
it('should keep image relative with considering output directory in page format', function() {
|
||||
var LEXED = loadPage('IMAGES', {
|
||||
// GitHub repo ID
|
||||
repo: 'GitBookIO/javascript',
|
||||
|
||||
// Imaginary folder of markdown file
|
||||
dir: 'syntax',
|
||||
outdir: './'
|
||||
@@ -150,3 +113,11 @@ describe('Relative images', function() {
|
||||
assert(LEXED[0].content.indexOf('"preview2.png"') !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Section parsing', function() {
|
||||
it('should not have false positive quiz parsing', function() {
|
||||
var LEXED = loadPage('FALSE_QUIZ');
|
||||
|
||||
assert.equal(LEXED[0].type, 'normal');
|
||||
});
|
||||
});
|
||||
|
||||
+10
-1
@@ -12,7 +12,7 @@ describe('Plugin validation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plugin list of names', function () {
|
||||
describe('Plugins list', function () {
|
||||
var firstDefault = _.first(Plugin.defaults);
|
||||
|
||||
it('should convert string to array', function() {
|
||||
@@ -27,6 +27,15 @@ describe('Plugin list of names', function () {
|
||||
it('should remove name starting with -', function() {
|
||||
assert(!_.contains(Plugin.normalizeNames(["-"+firstDefault]), firstDefault));
|
||||
});
|
||||
|
||||
it('should accept version', function() {
|
||||
var _name = "test@0.3.0,exercises@1.2.0,test2";
|
||||
var plugins = Plugin.normalizeList(_name);
|
||||
|
||||
assert(_.find(plugins, {'name': "test"}).version = "0.3.0");
|
||||
assert(_.find(plugins, {'name': "exercises"}).version = "1.2.0");
|
||||
assert(!_.find(plugins, {'name': "test2"}).version);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plugin defaults loading', function () {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -31,19 +31,57 @@ define([
|
||||
return d.promise();
|
||||
}
|
||||
|
||||
var pregQuote = function( str ) {
|
||||
// http://kevin.vanzonneveld.net
|
||||
// + original by: booeyOH
|
||||
// + improved by: Ates Goral (http://magnetiq.com)
|
||||
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||
// + bugfixed by: Onno Marsman
|
||||
// * example 1: preg_quote("$40");
|
||||
// * returns 1: '\$40'
|
||||
// * example 2: preg_quote("*RRRING* Hello?");
|
||||
// * returns 2: '\*RRRING\* Hello\?'
|
||||
// * example 3: preg_quote("\\.+*?[^]$(){}=!<>|:");
|
||||
// * returns 3: '\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:'
|
||||
$.fn.replaceText = function( search, replace, text_only ) {
|
||||
return this.each(function(){
|
||||
var node = this.firstChild,
|
||||
val,
|
||||
new_val,
|
||||
|
||||
// Elements to be removed at the end.
|
||||
remove = [];
|
||||
|
||||
// Only continue if firstChild exists.
|
||||
if ( node ) {
|
||||
|
||||
// Loop over all childNodes.
|
||||
do {
|
||||
|
||||
// Only process text nodes.
|
||||
if ( node.nodeType === 3 ) {
|
||||
|
||||
// The original node value.
|
||||
val = node.nodeValue;
|
||||
|
||||
// The new value.
|
||||
new_val = val.replace( search, replace );
|
||||
|
||||
// Only replace text if the new value is actually different!
|
||||
if ( new_val !== val ) {
|
||||
|
||||
if ( !text_only && /</.test( new_val ) ) {
|
||||
// The new value contains HTML, set it in a slower but far more
|
||||
// robust way.
|
||||
$(node).before( new_val );
|
||||
|
||||
// Don't remove the node yet, or the loop will lose its place.
|
||||
remove.push( node );
|
||||
} else {
|
||||
// The new value contains no HTML, so it can be set in this
|
||||
// very fast, simple way.
|
||||
node.nodeValue = new_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} while ( node = node.nextSibling );
|
||||
}
|
||||
|
||||
// Time to remove those elements!
|
||||
remove.length && $(remove).remove();
|
||||
});
|
||||
};
|
||||
|
||||
var pregQuote = function( str ) {
|
||||
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
|
||||
};
|
||||
|
||||
@@ -59,13 +97,10 @@ define([
|
||||
var replaceTerm = function($el, term) {
|
||||
var r = new RegExp( "\\b(" + pregQuote(term.name.toLowerCase()) + ")\\b" , 'gi' );
|
||||
|
||||
$el.find("p:contains('"+term.name+"')").each( function( i, element ) {
|
||||
element = $(element);
|
||||
var content = $(element).html();
|
||||
|
||||
content = content.replace(r, '<span class="glossary-term" data-glossary-term="' + term.id + '" title="' + term.description + '">$1</span>');
|
||||
element.html(content);
|
||||
$el.find("*").replaceText(r, function(match) {
|
||||
return "<span class='glossary-term' data-glossary-term='"+term.id+"' title='"+term.description+"'>"+match+"</span>";
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
var prepare = function() {
|
||||
|
||||
@@ -7,7 +7,6 @@ define([
|
||||
var $book = $(dom.find(".book"));
|
||||
|
||||
state.$book = $book;
|
||||
state.githubId = $book.data("github");
|
||||
state.level = $book.data("level");
|
||||
state.basePath = $book.data("basepath");
|
||||
state.revision = $book.data("revision");
|
||||
|
||||
@@ -18,11 +18,6 @@ define([
|
||||
var $book;
|
||||
$book = state.$book;
|
||||
|
||||
if (state.githubId) {
|
||||
// Initialize storage
|
||||
storage.setBaseKey(state.githubId);
|
||||
}
|
||||
|
||||
// Init sidebar
|
||||
sidebar.init();
|
||||
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
define([
|
||||
"jQuery"
|
||||
], function($) {
|
||||
var url = location.href;
|
||||
var title = $("title").text();
|
||||
|
||||
var types = {
|
||||
"twitter": function($el) {
|
||||
window.open("http://twitter.com/home?status="+encodeURIComponent(title+" "+url))
|
||||
window.open("http://twitter.com/home?status="+encodeURIComponent($("title").text()+" "+location.href))
|
||||
},
|
||||
"facebook": function($el) {
|
||||
window.open("http://www.facebook.com/sharer/sharer.php?s=100&p[url]="+encodeURIComponent(url))
|
||||
window.open("http://www.facebook.com/sharer/sharer.php?s=100&p[url]="+encodeURIComponent(location.href))
|
||||
},
|
||||
"google-plus": function($el) {
|
||||
window.open("https://plus.google.com/share?url="+encodeURIComponent(url))
|
||||
window.open("https://plus.google.com/share?url="+encodeURIComponent(location.href))
|
||||
},
|
||||
"weibo": function($el) {
|
||||
window.open("http://service.weibo.com/share/share.php?content=utf-8&url="+encodeURIComponent(url)+"&title="+encodeURIComponent(title))
|
||||
window.open("http://service.weibo.com/share/share.php?content=utf-8&url="+encodeURIComponent(location.href)+"&title="+encodeURIComponent($("title").text()))
|
||||
},
|
||||
"instapaper": function($el) {
|
||||
window.open("http://www.instapaper.com/text?u="+encodeURIComponent(url));
|
||||
window.open("http://www.instapaper.com/text?u="+encodeURIComponent(location.href));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ol ul {
|
||||
ol ul,
|
||||
ul ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -3,30 +3,61 @@
|
||||
@import "ebook/variables.less";
|
||||
@import "ebook/highlight.less";
|
||||
|
||||
.book-chapter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.exercise, .quiz {
|
||||
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;
|
||||
.page {
|
||||
&.page-toc {
|
||||
ol {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.question {
|
||||
margin-top: 0.4cm;
|
||||
.book-chapter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.exercise, .quiz {
|
||||
margin: 1cm 0cm;
|
||||
border: 2px solid #ddd;
|
||||
|
||||
.exercise-header {
|
||||
padding: 0.1cm 0.3cm;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: bold;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.exercise-inner {
|
||||
padding: 0.15cm 0.3cm;
|
||||
|
||||
p:last-child {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.question {
|
||||
margin-top: 0.1cm;
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
.question-base {
|
||||
|
||||
}
|
||||
|
||||
.question-solution {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
.markdown-content(#333, 1.6);
|
||||
.page {
|
||||
font-family: sans-serif;
|
||||
.markdown-content(#333, 1.6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "site.html" %}
|
||||
{% extends "page.html" %}
|
||||
|
||||
{% block title %}Glossary | {{ title }}{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "layout.html" %}
|
||||
{% extends "../layout.html" %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
{% extends "layout.html" %}
|
||||
{% extends "../layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
{% parent %}
|
||||
{% if githubAuthor %}
|
||||
<meta name="author" content="{{ githubAuthor }}">
|
||||
{% endif %}
|
||||
{% if progress.current.next and progress.current.next.path %}
|
||||
<link rel="next" href="{{ basePath }}/{{ progress.current.next.path|mdLink }}" />
|
||||
{% endif %}
|
||||
@@ -17,11 +14,11 @@
|
||||
{% block description %}{% if progress.current.level == "0" %}{{ description }}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="book" {% if githubId %}data-github="{{ githubId }}"{% endif %} data-level="{{ progress.current.level }}" data-basepath="{{ basePath }}" data-revision="{{ revision }}">
|
||||
{% include "includes/book/summary.html" %}
|
||||
<div class="book" data-level="{{ progress.current.level }}" data-basepath="{{ basePath }}" data-revision="{{ revision }}">
|
||||
{% include "includes/summary.html" %}
|
||||
<div class="book-body">
|
||||
<div class="body-inner">
|
||||
{% include "includes/book/header.html" %}
|
||||
{% include "includes/header.html" %}
|
||||
<div class="page-wrapper" tabindex="-1">
|
||||
<div class="page-inner">
|
||||
{% block page_inner %}
|
||||
@@ -30,9 +27,9 @@
|
||||
{% if section.type == "normal" %}
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
{% elif section.type == "exercise" %}
|
||||
{% include "./includes/book/exercise.html" with {section: section} %}
|
||||
{% include "./includes/exercise.html" with {section: section} %}
|
||||
{% elif section.type == "quiz" %}
|
||||
{% include "./includes/book/quiz.html" with {section: section} %}
|
||||
{% include "./includes/quiz.html" with {section: section} %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "./layout.html" %}
|
||||
|
||||
{% block title %}Glossary | {{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page page-toc">
|
||||
<h1>Glossary</h1>
|
||||
{% for item in glossaryIndex %}
|
||||
<section class="normal glossary" id="{{ item.id }}">
|
||||
<h2><a href="#{{ item.id }}">{{ item.name }}</a></h2>
|
||||
<p>{{ item.description }}</p>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="exercise-header">Exercise</div>
|
||||
<div class="exercise-inner">
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
</div>
|
||||
<div class="exercise-inner">
|
||||
<pre><code>{% autoescape false %}{{ section.code.base|code }}{% endautoescape %}</code></pre>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="exercise-inner">
|
||||
<pre><code>{% autoescape false %}{{ section.code.solution|code }}{% endautoescape %}</code></pre>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="exercise-header">Quiz</div>
|
||||
<div class="exercise-inner">{% autoescape false %}{{ section.content }}{% endautoescape %}</div>
|
||||
{% for quiz in section.quiz %}
|
||||
<div class="question exercise-inner">
|
||||
<div class="question-header">Question {{ loop.index }} of {{ section.quiz.length }}</div>
|
||||
|
||||
<div class="question-base">
|
||||
{% autoescape false %}{{ quiz.base }}{% endautoescape %}
|
||||
</div>
|
||||
<div class="question-solution">
|
||||
{% autoescape false %}{{ quiz.solution }}{% endautoescape %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en-US" {% block htmlTag %}{% endblock %}>
|
||||
{{ htmlSnippet("html:start")|default("") }}
|
||||
<head>
|
||||
{{ htmlSnippet("head:start")|default("") }}
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{% block description %}{% endblock %}">
|
||||
<meta name="generator" content="GitBook {{ gitbook.version }}">
|
||||
{% block head %}{% endblock %}
|
||||
{{ htmlSnippet("head:end")|default("") }}
|
||||
</head>
|
||||
<body>
|
||||
{{ htmlSnippet("body:start")|default("") }}
|
||||
{% block style %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block javascript %}{% endblock %}
|
||||
{{ htmlSnippet("body:end")|default("") }}
|
||||
</body>
|
||||
{{ htmlSnippet("html:end")|default("") }}
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "./layout.html" %}
|
||||
|
||||
{% block title %}{{ progress.current.title }} | {{ title }}{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{{ staticBase }}/print.css">
|
||||
{% for resource in plugins.resources.css %}
|
||||
{% if resource.url %}
|
||||
<link rel="stylesheet" href="{{ resource.url }}">
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="{{ staticBase }}/plugins/{{ resource.path }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<h1 class="book-chapter book-chapter-{{ progress.current.level|lvl }}">{{ progress.current.title }}</h1>
|
||||
{% for section in content %}
|
||||
<div class="{{ section.type }}" id="section-{{ section.id }}">
|
||||
{% if section.type == "normal" %}
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
{% elif section.type == "exercise" %}
|
||||
{% include "./includes/exercise.html" with {section: section} %}
|
||||
{% elif section.type == "quiz" %}
|
||||
{% include "./includes/quiz.html" with {section: section} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends "./layout.html" %}
|
||||
|
||||
{% block title %}Table of Contents | {{ title }}{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{{ staticBase }}/print.css">
|
||||
{% for resource in plugins.resources.css %}
|
||||
{% if resource.url %}
|
||||
<link rel="stylesheet" href="{{ resource.url }}">
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="{{ staticBase }}/plugins/{{ resource.path }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% macro articles(_articles) %}
|
||||
{% for item in _articles %}
|
||||
{% set externalLink = item.path|isExternalLink %}
|
||||
<li>
|
||||
{% if item.path %}
|
||||
{% if !externalLink %}
|
||||
<a href="{{ basePath }}/{{ item.path|mdLink }}">{{ item.title }}</a>
|
||||
{% else %}
|
||||
<a target="_blank" href="{{ item.path }}">{{ item.title }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ item.title }}
|
||||
{% endif %}
|
||||
{% if item.articles.length > 0 %}
|
||||
<ol>
|
||||
{{ articles(item.articles) }}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page page-toc">
|
||||
<h1>Table of Contents</h1>
|
||||
<ol>
|
||||
{{ articles(summary.chapters) }}
|
||||
|
||||
{% if glossary.length > 0 %}
|
||||
<li><a href="{{ basePath }}/GLOSSARY.html">Glossary</a></li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<head>
|
||||
{{ htmlSnippet("head:start")|default("") }}
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=11; IE=10; IE=9; IE=8; IE=7; IE=EDGE" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{% block description %}{% endblock %}">
|
||||
@@ -14,6 +15,9 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ staticBase }}/images/apple-touch-icon-precomposed-152.png">
|
||||
<link rel="shortcut icon" href="{{ staticBase }}/images/favicon.ico" type="image/x-icon">
|
||||
{% if options.isbn %}
|
||||
<meta name="identifier" content="{{ options.isbn }}" scheme="ISBN">
|
||||
{% endif %}
|
||||
{% block head %}{% endblock %}
|
||||
{{ htmlSnippet("head:end")|default("") }}
|
||||
</head>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{{ staticBase }}/print.css">
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro articleContent(content) %}
|
||||
{% for section in content %}
|
||||
{% if section.type == "normal" %}
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
{% elif section.type == "exercise" %}
|
||||
<div class="exercise">
|
||||
<div class="exercise-header">Exercise #{{ exercise }}</div>
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
<pre><code>{% autoescape false %}{{ section.code.base|code }}{% endautoescape %}</code></pre>
|
||||
{% set exercise = exercise + 1 %}
|
||||
</div>
|
||||
{% elif section.type == "quiz" %}
|
||||
<div class="quiz">
|
||||
<div class="exercise-header">Quiz #{{ exercise }}</div>
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
{% for quiz in section.quiz %}
|
||||
<div class="question">
|
||||
<div class="question-header">Question {{ loop.index }} of {{ section.quiz.length }}</div>
|
||||
{% autoescape false %}{{ quiz.base }}{% endautoescape %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% set exercise = exercise + 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% set exercise = 1 %}
|
||||
{% block content %}
|
||||
{# Pages content #}
|
||||
{% for item in progress.chapters %}
|
||||
<article id="{{ article.path }}">
|
||||
<h1 class="book-chapter book-chapter-{{ item.level|lvl }}">{{ item.title }}</h1>
|
||||
{% if pages[item.path] %}
|
||||
{{ articleContent(pages[item.path].content) }}
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{# Exercise solutions #}
|
||||
{% if exercise > 1 %}
|
||||
{% set exercise = 1 %}
|
||||
<article>
|
||||
<h1 class="book-chapter book-chapter-1">Exercise Solutions</h1>
|
||||
<h1>Exercise Solutions</h1>
|
||||
|
||||
{% for item in progress.chapters %}
|
||||
{% if pages[item.path] %}
|
||||
{% for section in pages[item.path].content %}
|
||||
{% if section.type == "exercise" %}
|
||||
<div class="exercise">
|
||||
<div class="exercise-header">Exercise #{{ exercise }}</div>
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
<pre><code>{% autoescape false %}{{ section.code.solution|code }}{% endautoescape %}</code></pre>
|
||||
{% set exercise = exercise + 1 %}
|
||||
</div>
|
||||
{% elif section.type == "quiz" %}
|
||||
<div class="quiz">
|
||||
<div class="exercise-header">Quiz #{{ exercise }}</div>
|
||||
{% autoescape false %}{{ section.content }}{% endautoescape %}
|
||||
{% for quiz in section.quiz %}
|
||||
<div class="question">
|
||||
<div class="question-header">Question {{ loop.index }} of {{ section.quiz.length }}</div>
|
||||
{% autoescape false %}{{ quiz.solution }}{% endautoescape %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% set exercise = exercise + 1 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}{% endblock %}
|
||||
Reference in New Issue
Block a user