Compare commits

...

214 Commits

Author SHA1 Message Date
Samy Pesse 33d7e022e6 Bump version to 3.2.1 2016-09-28 13:21:24 +02:00
Samy Pesse 09782b964b Update theme-default@1.0.6 2016-09-28 13:10:47 +02:00
Johan Preynat 7938ee8ec6 Use nunjucks@2.5.2 2016-09-28 10:32:14 +02:00
Peter B Smith 2e8e4f63d7 fixing a typo of the word "publishing" (#1536) 2016-09-28 09:49:29 +02:00
jdaviescoates b9fa6c3051 Update examples.md (#1522)
By adding the Loomio and Enspiral handbook examples
2016-09-08 20:38:59 +02:00
Samy Pessé 9c071dade5 Merge pull request #1481 from mcveat/patch-1
Fixed typo
2016-08-04 00:19:11 +02:00
Piotr Adamski 56e44651fa Fixed typo 2016-08-03 22:08:12 +02:00
Samy Pessé 42fac98992 Fix command for getting gitbook version 2016-08-01 09:47:31 +02:00
Samy Pessé 4f5513add0 Create template for GitHub issues 2016-07-25 20:39:31 +02:00
Samy Pesse 90cbfc0596 Add OSX testing on Travis 2016-07-15 11:38:58 +02:00
Samy Pessé 38ab9fa05a Bump version to 3.2.0 2016-07-11 16:48:13 +02:00
Samy Pessé 12e20c5fcc Update theme-default@1.0.5 2016-07-11 16:47:16 +02:00
Samy Pessé 6b103f4532 Fix #1408: fix i18n in website generator and add tests for it 2016-07-11 16:27:22 +02:00
Soreine d4fa5c18a8 editPlugin and getPluginConfig 2016-07-11 15:18:35 +02:00
Soreine 2a116dfdb0 Fix reducedObject when default property is undefined 2016-07-11 15:18:35 +02:00
Samy Pessé 0349f54f3c Export file model 2016-07-07 16:41:05 +02:00
Samy Pessé 695bda312c Bump version to 3.2.0-pre.1 2016-07-05 17:15:48 +02:00
Samy Pessé 144bd4c97a Update gitbook-markdown@2.0.1 2016-07-05 11:07:51 +02:00
Samy Pessé e105983cf8 Add back property "output" to page/template context 2016-07-04 18:08:44 +02:00
Soreine ea3afbed75 Update gitbook-plugin-lunr@1.2.0 2016-07-01 17:52:15 +02:00
Soreine d563abe3df Reduce size of logo to give room to version number 2016-06-30 15:37:31 +02:00
Soreine 70fd4ded29 Update theme-official for doc 2016-06-30 12:15:50 +02:00
Samy Pessé 9ce9388dfc Bump version to 3.2.0-pre.0 2016-06-30 11:09:18 +02:00
Samy Pessé c575ad83ae Update gitbook-markdown@2.0.0 2016-06-30 10:51:46 +02:00
Samy Pessé 936fa1545b Add tests for locationUtils.flatten 2016-06-29 10:32:49 +02:00
Samy Pessé c9c2dde2b6 Add method "isFile" and "isReadme" in SummaryArticle 2016-06-29 10:31:06 +02:00
Samy Pessé d9a1d387c7 Merge pull request #1397 from mpfeil/master
add open and browser args to serve command
2016-06-23 11:40:05 +02:00
Matthias Pfeil bf29d187c8 disable open by default 2016-06-20 17:24:08 +02:00
Matthias Pfeil d86f9b2667 add open and browser args to serve command 2016-06-17 16:00:53 +02:00
Samy Pesse aca4313fc3 Enable long stack support when process.env.CI is true 2016-06-11 12:38:19 +02:00
Samy Pesse 4cb5bb06fb Bump version to 3.1.1 2016-06-10 22:19:09 +02:00
Samy Pesse 24988b75ea Fix extsAsciidoc.js 2016-06-10 22:14:04 +02:00
Samy Pesse 775e858d05 Expose markdown/asciidoc extensions to gitbook module 2016-06-10 22:13:31 +02:00
Samy Pesse 1e3d871c1e Merge branch 'fix/plugins_loading' 2016-06-10 22:08:16 +02:00
Samy Pesse 94a4310c1e Adapt Plugins.loadForBook to use correct order 2016-06-10 22:04:19 +02:00
Samy Pessé e8ee57be83 Start fixing order of plugins in dependencies 2016-06-10 18:34:24 +02:00
Samy Pessé 50a132cb2c Add method "createChildLevel" to SummaryPart 2016-06-10 18:11:22 +02:00
Samy Pessé 59be32a54f Add method .createChildLevel for SummaryArticle 2016-06-10 18:08:56 +02:00
Soreine 4ae7cdb5d1 Mention autocover plugin in docs 2016-06-10 18:03:40 +02:00
Samy Pessé 990a4efeb5 Update gitbook-markdown and gitbook-asciidoc 2016-06-10 17:53:10 +02:00
Samy Pessé 9c927dcf13 Fix #1374 and #1360: apply transform of conrefsLoader using a string, not a buffer 2016-06-10 17:45:30 +02:00
Samy Pessé 7a08b3f109 Fix #1377: fix listing of plugins when using gitbook as a library 2016-06-10 17:15:04 +02:00
Samy Pessé 94d4234cb8 Merge branch 'fix/plugins_order' 2016-06-10 16:44:13 +02:00
Samy Pessé fbc851bc4e Fix listAll to add default plugins after normal ones 2016-06-10 16:43:32 +02:00
Samy Pessé e92b661a2a Adapt sortPlugins to only sort an ordered map of plugins 2016-06-10 16:37:44 +02:00
Soreine b9fe60915f Add tests for insertPart 2016-06-10 15:40:51 +02:00
Samy Pessé 1bf7d092f0 Merge pull request #1363 from todvora/master
Fixed target attribute for external links
2016-06-10 00:28:50 +02:00
Samy Pesse 826d472511 Fix documentation about themes 2016-06-08 20:06:50 +02:00
Samy Pessé a6b1f4bd87 Compare list of plugins sorted to array of names in tests 2016-06-08 15:08:11 +02:00
Samy Pessé 004f91f02c sortPlugins should not be async 2016-06-08 15:02:34 +02:00
Samy Pessé e6c6ec4293 Improve testing for sortPlugins 2016-06-08 14:57:54 +02:00
Soreine a1d3acf065 Fix serve cli using new ConfigModifier.addPlugin 2016-06-08 10:42:27 +02:00
Soreine a732660f59 docs: cover remove extraneous line 2016-06-08 10:35:12 +02:00
Nicolas Gaborit e1bb0638f8 docs: cover, fix formatting 2016-06-08 10:16:54 +02:00
Soreine 788c0c7e7e Adds cover.jpg info 2016-06-08 10:12:42 +02:00
Tomas Dvorak 762fe16a17 fixed target attribute for external links 2016-06-07 18:34:36 +02:00
Samy Pessé 15123805d1 Fix linting error 2016-06-07 16:24:22 +02:00
Samy Pessé 23aecb4f2b Improve output of page to text with frontmatter 2016-06-07 16:23:12 +02:00
Samy Pessé f1d200b81b Rollback frontmatter parsing to js-yaml to be browserify compatible 2016-06-07 14:32:12 +02:00
Samy Pessé b566711ee1 Add method toText to Page 2016-06-07 12:51:35 +02:00
Samy Pessé 22da5aac11 Switch frontmatter parsing to gray-matter 2016-06-07 12:47:16 +02:00
Samy Pessé f7b95102c5 Expose Page model 2016-06-07 12:36:32 +02:00
Samy Pessé ca70c934d6 Split page parsing into parsePageFromString and parsePage
Add tests for parsePageFromString
2016-06-07 12:31:21 +02:00
Samy Pessé 1cc48fa314 Use Immutable instead of lodash for mergeDefaults 2016-06-07 12:06:29 +02:00
Samy Pessé b63b5574d3 Only merge objects in mergeDefaults 2016-06-07 11:40:54 +02:00
Samy Pessé 67106e8698 Improve plugins config modifier 2016-06-07 11:06:53 +02:00
Samy Pessé dc2ab51a88 Update i18n-t and juice 2016-06-07 10:54:34 +02:00
Samy Pessé cedfa186f2 Remove merge-defaults dependency 2016-06-07 10:48:04 +02:00
Samy Pessé 80f0d1bfed Update gitbook-markdown and gitbook-asciidoc to use lodash@4 2016-06-07 10:36:01 +02:00
Samy Pessé 7178ab1f82 Improve Config.toText to return only the required fields 2016-06-07 10:26:28 +02:00
Samy Pesse bf895b707e Improve addPlugin to not append default plugins to the list 2016-06-07 00:56:24 +02:00
Samy Pesse 996170a4ad ConfigModifier.removePlugin should toggle off if default 2016-06-07 00:48:47 +02:00
Samy Pesse aa66a4f928 Expose DEFAULT_PLUGINS as a list of PluginDependency 2016-06-06 22:04:13 +02:00
Samy Pesse 23010fa54a Adapt tests for config modifiers 2016-06-06 21:22:44 +02:00
Samy Pesse 0baebc7979 Change config modifier to work on a config object 2016-06-06 21:20:00 +02:00
Samy Pesse 257158321c Bump version to 3.1.0 2016-06-06 19:51:44 +02:00
Samy Pesse ddf0a41e29 Add method editPlugin to config modifiers 2016-06-06 19:42:44 +02:00
Samy Pesse 357e1e8e96 Add unit tests for postRender 2016-06-06 19:24:12 +02:00
Samy Pesse 7a89f9869a Update gitbook-markdown@1.3.0 and gitbook-asciidoc@1.2.0 2016-06-06 18:07:31 +02:00
Samy Pesse 30fbd22efe Add modifier togglePlugin 2016-06-06 17:27:47 +02:00
Samy Pesse 29df73ba72 Add method "toText" to config 2016-06-06 17:16:53 +02:00
Soreine f8676483c1 Default paths are relative to the current book 2016-06-06 16:55:00 +02:00
Johan Preynat c79a6f2fba Use theme-default@1.0.4 2016-06-06 15:46:44 +02:00
Samy Pessé c38e67ea61 Merge pull request #1344 from GitbookIO/fix/readme-in-summary
Search for README file in whole summary before inserting as first article
2016-06-06 12:18:06 +02:00
Johan Preynat 97a5f8805e Use plugin-fontsettings@2.0.0 2016-06-02 10:24:12 +02:00
Johan Preynat 421fef6ebf Search for README file in whole summary before inserting as first article 2016-05-30 15:07:39 +02:00
Johan Preynat 33406c3082 Merge pull request #1336 from GitbookIO/loading/themes
Load plugins before themes
2016-05-28 11:56:34 +02:00
Johan Preynat c3851889b0 Throw an error if generateMock() cannot create the temp directory 2016-05-27 17:15:23 +02:00
Johan Preynat 4a5de2da6b Use sortPlugins() in loadForBook() 2016-05-27 15:06:20 +02:00
Johan Preynat 1afb23cada Add test file for sortPlugins() 2016-05-27 15:06:08 +02:00
Johan Preynat a3b0f3f868 Export plugins sorting method to load plugins before themes 2016-05-27 15:05:55 +02:00
Johan Preynat 0aa32916d6 Add prefix for theme plugins as constant 2016-05-27 15:05:33 +02:00
Samy Pessé ebe845d46c Bump version to 3.0.3 2016-05-27 12:16:56 +02:00
Samy Pessé edee8d2fc5 Update gitbook-theme-default@1.0.3 2016-05-27 12:07:35 +02:00
Samy Pessé 5f3de0876f Add header 'X-Current-Location' to serve and fix handling of directories 2016-05-27 12:01:28 +02:00
Samy Pessé 2ceab04d01 Increase timeout for tests of plugin installation 2016-05-27 11:40:05 +02:00
Samy Pessé de37de44e8 Add tests for installation of all plugins 2016-05-27 11:18:12 +02:00
Samy Pessé ccb87d5a13 Add unit tests for installPlugin 2016-05-27 11:10:39 +02:00
Samy Pessé e2e0ca3f05 Add test for plugin version resolution 2016-05-27 10:54:56 +02:00
Samy Pessé b33a79704b Switch plugin installation and resolveVersion to use PluginDependency instead of Plugin 2016-05-27 09:38:23 +02:00
Samy Pessé 6def174b84 Merge pull request #1334 from ryanswanson/master
Fixed two issues for handling git URLs for plugins
2016-05-27 09:27:11 +02:00
Samy Pessé 346a18b444 Merge pull request #1335 from GitbookIO/fix/1333
Fix #1333: fix invalid URLs for glossary annotations
2016-05-26 22:30:06 +02:00
Samy Pesse b32685c698 Add test for custom glossary file 2016-05-26 22:01:40 +02:00
Samy Pesse 09b3618da8 Annotate glossary terms before resolving links 2016-05-26 21:38:27 +02:00
Samy Pesse 901328f247 Complete basic tests for glossary 2016-05-26 21:37:10 +02:00
Samy Pesse b47ebaa9d9 Use real glossary name when annotating 2016-05-26 21:10:06 +02:00
Ryan Swanson 465e8d6c0a Fixed two issues for handling git URLs for plugins that are published to git repositories rather than on npmjs.org.
Fixed issue in pluginDependency to dereference 'name' and 'version' using Immutable Map.get(...) accessors since config values are now wrapped by Immutable.fromJS(...) in config.js > setValue(...).  Added associated unit test.

Fixed issue in resolveVersion where a plugin may be using a git URL rather than a semver for the version portion of the plugin config definition.  In addition, refactored the resolveVersion function into a new module to allow for unit testing.  Added associated unit test.
2016-05-26 14:30:29 -04:00
Samy Pessé ecd86ec877 Method for parsing glossary when not listed in summary 2016-05-26 17:26:11 +02:00
Samy Pessé 5d7864154c Start test for glossary generation 2016-05-26 17:25:49 +02:00
Samy Pessé 7b915428f7 Fix #1316: fix regression for supporting uppercase filenames in structure 2016-05-26 11:07:13 +02:00
Samy Pesse 0bce3517fd Bump version to 3.0.2 2016-05-25 21:50:38 +02:00
Samy Pesse 089815a8fd Fix #1328: fix crash when anchor links 2016-05-25 21:49:58 +02:00
Samy Pesse 7e8f1a9632 Bump version to 3.0.1 2016-05-25 20:00:04 +02:00
Samy Pesse 8469648241 Update theme-default@1.0.2 2016-05-25 17:14:15 +02:00
Samy Pessé e7a44358a5 Update Changelog 2016-05-24 15:38:26 +02:00
Samy Pessé fb930d889f Fix anchor breaking links resolution 2016-05-24 15:32:30 +02:00
Samy Pessé 721a75309b Fix #1316: adapt regexp to handle dot in filenames 2016-05-23 13:00:01 +02:00
Samy Pessé b856bff91b Add failing test for #1316 2016-05-23 12:48:46 +02:00
Samy Pesse 0744f000f9 Fix doc for conrefs 2016-05-22 19:59:53 +02:00
Samy Pesse d1e226dba5 Bump version to 3.0.0 2016-05-20 22:18:37 +02:00
Samy Pesse 1923d28d71 Update dependencies 2016-05-20 22:17:44 +02:00
Samy Pessé 484a306adb Fix error with isInRoot when empty root 2016-05-20 21:41:02 +02:00
Samy Pessé 5bfc9f2f56 Fix PathUtils.isInRoot on windows 2016-05-20 21:37:22 +02:00
Samy Pesse 86a65e5287 Improve appveyor configuration 2016-05-20 19:23:51 +02:00
Samy Pesse 956d301dd7 Add node v6 to appveyor matrix 2016-05-20 18:22:44 +02:00
Samy Pesse 00d1a7b5a6 Update theme-default@1.0.1 2016-05-20 18:17:34 +02:00
Samy Pessé 2718c9c017 Fix test command for windows 2016-05-19 10:45:25 +02:00
Samy Pesse 3234c1e03f Fix #1310: enforce language config in language books 2016-05-19 00:15:09 +02:00
Aaron O'Mullan c8f8eece92 Merge pull request #1308 from arebee/patch-1
Fix english phrasing for Node version.
2016-05-19 00:10:08 +02:00
Richard Burte 7394efa9dc Fix english phrasing for Node version. 2016-05-18 09:54:04 -07:00
Samy Pesse 3e4241fc88 Implement renderInline method and missing output methods for API 2016-05-17 19:45:48 +02:00
Samy Pesse a5ae79604c Implement renderBlock method for plugin API 2016-05-17 19:30:28 +02:00
Soreine fc4bdd68a1 Can only use file names 2016-05-17 16:47:52 +02:00
Soreine f83975b022 Mention the structure configuration variable 2016-05-17 16:37:13 +02:00
Soreine 167d3957bf Expose lookup method for parsable files 2016-05-17 12:48:03 +02:00
Samy Pessé a9571fd67d Bump version to 3.0.0-pre.15 2016-05-12 16:53:14 +02:00
Samy Pessé ac87f48ba5 Update theme-default@1.0.0 2016-05-12 16:52:19 +02:00
Samy Pessé 05403f527d Fix bug that can happen when configuration file doesn't exist 2016-05-12 16:45:09 +02:00
Samy Pessé 53c72f0806 Add api method getPageByPath for plugins 2016-05-12 16:29:36 +02:00
Samy Pessé c8c2a61f96 Fix #1289: add option to filter files when listing using fs.listAllFiles 2016-05-12 16:10:11 +02:00
Samy Pessé 08d780d1f6 Fix #1300: parse page frontmatter and content when listing pages 2016-05-12 15:48:02 +02:00
Samy Pessé 2e74fafa36 Add template method "getPageByPath" 2016-05-12 15:12:08 +02:00
Samy Pessé 28ac9ab88a Fix #1299: pass fileExists as global function for template, accept null/undefined as param 2016-05-12 15:04:30 +02:00
Samy Pessé 26acab4dfb Fix generation of ebook for multilingual book (gitbook pdf) 2016-05-12 14:29:25 +02:00
Samy Pessé d4b562e001 Fix get of cover for multilingual books 2016-05-12 14:12:43 +02:00
Soreine 61956e0646 Revert "Extend Book.getLanguageBook"
This reverts commit 7892259fe6.
2016-05-12 14:08:29 +02:00
Soreine 7892259fe6 Extend Book.getLanguageBook 2016-05-12 13:35:29 +02:00
Samy Pessé b97d1c7796 Fix languages not being added to json output 2016-05-12 13:25:09 +02:00
Samy Pessé a091c5b331 Fix path resolution when generating page with conrefs 2016-05-12 12:19:00 +02:00
Samy Pessé 784b3379b5 Improve logging when generating multilingual book 2016-05-12 11:20:27 +02:00
Johan Preynat 62446e75d2 Update book's root folder definition in Changelog 2016-05-12 11:18:00 +02:00
Samy Pessé 8aa2f3075c Fix #1294: multilingual book should extend the book's config 2016-05-12 11:15:13 +02:00
Samy Pessé 839ee0385a Add method "getLanguageBook" to book model 2016-05-12 11:02:37 +02:00
Samy Pesse d6b60c6116 Adapt eslint configuration for mocha 2016-05-11 16:56:18 +02:00
Samy Pesse 9013f02850 Add test for transform option of ConrefLoader 2016-05-11 15:08:10 +02:00
Samy Pesse 961d43f085 Fix #1293: replace block shortcuts when including files 2016-05-11 14:45:54 +02:00
Samy Pessé 7bd49606e3 Merge pull request #1291 from GitbookIO/summary-modifiers
Summary modifiers
2016-05-11 13:57:21 +02:00
Samy Pesse d7c8635350 Increase mocha timeout 2016-05-11 13:57:00 +02:00
Soreine 19e9ff81d2 Tweak insertPart signature 2016-05-11 13:11:32 +02:00
Soreine e137bf632a Adds editArticleRef 2016-05-11 13:11:32 +02:00
Soreine 2fef898a1e Modifier moveArticleAfter 2016-05-11 13:11:32 +02:00
Soreine 0945807184 Greatly simplify moveArticle's implementation 2016-05-11 13:11:32 +02:00
Soreine d85663b153 Tests remove/insert articles and update insert signature 2016-05-11 13:11:31 +02:00
Soreine 481a7d790c Insert/remove part 2016-05-11 13:11:31 +02:00
Samy Pesse ef589a6b13 Switch tests to mocha while keeping jest structure 2016-05-11 13:02:24 +02:00
Samy Pesse d5c4af3377 Add immutable ignore instance for book 2016-05-11 13:02:24 +02:00
Soreine 0b4df4cdf2 Precise h2 headings for Glossary 2016-05-11 10:55:34 +02:00
Samy Pessé d6814dc48e Don't copy config file as asset 2016-05-10 15:23:59 +02:00
Samy Pessé ce50a1d50f Bump version to 3.0.0-pre.14 2016-05-10 14:37:22 +02:00
Samy Pessé a76e018d36 Update theme-default@1.0.0-pre.10 2016-05-10 14:37:05 +02:00
Samy Pessé b06f1fd677 Fix article.depth starting at 2 2016-05-10 14:36:03 +02:00
Samy Pessé 3d8b1880dc Fix page's title in pdf header/footer 2016-05-10 14:25:50 +02:00
Samy Pessé 1a13c8c9bb Add option "--no-live" for command serve 2016-05-10 13:14:32 +02:00
Samy Pessé 852fae3a09 Fix #1232: add option "--no-watch" to disable watcher and live reloading 2016-05-10 13:08:19 +02:00
Samy Pessé d342d31c8e Fix error when summary contains entry without ref 2016-05-10 12:12:19 +02:00
Samy Pesse 5b3f54fbc8 Fix "gitbook init" when article has no ref 2016-05-10 09:55:22 +02:00
Samy Pessé 308ece784c Merge pull request #1288 from todvora/patch-1
Fixed summary encoding
2016-05-10 08:55:40 +02:00
Tomas Dvorak 0758ddf04e Fixed summary encoding 2016-05-10 08:17:52 +02:00
Samy Pesse f24d9aef73 Add test for block replaced with anchors 2016-05-09 22:17:46 +02:00
Samy Pesse f8bb55f258 Adapt unit tests for blocks 2016-05-09 22:01:25 +02:00
Samy Pesse 76daebeb84 Fix unit test for templateBlock using a removed method "getParse" 2016-05-09 18:41:21 +02:00
Samy Pesse b0d366b7e8 Fix linting warning in models/templateBlock.js 2016-05-08 22:45:26 +02:00
Samy Pesse 3d0054dd64 Fix #1286: by default blocks should not be parsable 2016-05-08 22:33:41 +02:00
Samy Pessé 13bc89ff44 Merge pull request #1285 from GitbookIO/api/summary
[WIP] Api for plugins to access summary
2016-05-08 22:28:15 +02:00
Samy Pesse bb910f984e Improve comment for API summary.walk 2016-05-08 22:26:46 +02:00
Samy Pessé e576fdea14 Merge pull request #1278 from dylang/patch-1
bump bash-color to version that has a license
2016-05-08 18:44:53 +02:00
Samy Pesse c7864927ec Add method summary.getArticleByPath to summary api 2016-05-08 11:08:27 +02:00
Samy Pesse 54956de59f Start API for plugins to access the summary 2016-05-08 10:33:45 +02:00
Samy Pesse 2cbd30d1f2 Fix command "gitbook init" 2016-05-07 00:24:12 +02:00
Dylan Greene 55614f1675 bump bash-color to version that has a license
Version currently being used does not have a license making it difficult for companies to use.
2016-05-06 09:56:43 -04:00
Samy Pesse 4f7421993f Bump version to 3.0.0-pre.13 2016-05-06 10:59:52 +02:00
Samy Pesse 32ad7b530e Fix #1266: don't fail on "a" tags without "href" 2016-05-06 10:57:41 +02:00
Samy Pesse ee5d5010f6 Correctly read assets using input fs when copying 2016-05-06 10:54:13 +02:00
Samy Pesse cf7e5884cd Add method readAsStream on fs model 2016-05-06 10:53:55 +02:00
Samy Pesse 11a0a64d9e Add failing test for #1272 2016-05-06 10:36:55 +02:00
Samy Pesse b6051babac Adapt unit tests for new templating post process 2016-05-05 11:09:35 +02:00
Samy Pesse 34947b5e20 Add mode TemplateOutput to represent {content + blocks} from template 2016-05-05 11:00:05 +02:00
Samy Pesse c621380b66 Add tests for replaceShortcuts 2016-05-04 20:18:48 +02:00
Samy Pesse c4b54033ce Fix #1268: fix shortcuts of template block not being applied 2016-05-04 19:01:31 +02:00
Samy Pesse 4d85d6eb6e Fix #1267: don't fail when "node_modules" folder doesn't exist 2016-05-04 17:41:15 +02:00
Samy Pessé cc9274c9af Bump version to 3.0.0-pre.12 2016-05-04 13:50:52 +02:00
Johan Preynat bbdf7efc34 Fix location.relative(dir, file) trailing slash if file is a directory 2016-05-04 11:51:39 +02:00
Samy Pessé 38a986995a Merge pull request #1264 from GitbookIO/fix/plugin-resources
Fix Loading plugin resources
2016-05-04 10:33:29 +02:00
Johan Preynat ee8d35df7a Update Plugins.listResources() to use the prepared resources list 2016-05-04 10:30:11 +02:00
Johan Preynat be411cd5ff Update copyPluginAssets to use prepared resources 2016-05-04 10:27:21 +02:00
Johan Preynat 2104019171 List resources per plugin in website generator onInit() 2016-05-04 10:26:57 +02:00
Johan Preynat 4e564707df Add resources to website generator state 2016-05-04 10:26:06 +02:00
Johan Preynat da8611e0c3 Add global method resolve(fileName) 2016-05-04 10:25:37 +02:00
Samy Pessé 5d11641a5f Merge pull request #1260 from GitbookIO/move-article
Adds move and remove article modifier.
2016-05-04 09:47:50 +02:00
Samy Pessé 24d38e4fcf Fix #1263: Fix page generation for AsciiDoc 2016-05-04 09:30:09 +02:00
Samy Pessé efb65a8413 Remove filter for pdf/epub/mobi files when outputting 2016-05-03 17:18:13 +02:00
Soreine 3fc90554f5 Add tests, and fixes. 2016-05-03 16:02:58 +02:00
Soreine 4ebe28faae Update insert specification 2016-05-03 13:11:32 +02:00
Soreine 693171cb05 Adds move and remove article modifier. 2016-05-03 12:37:26 +02:00
205 changed files with 4442 additions and 1465 deletions
+4 -2
View File
@@ -13,8 +13,10 @@
"env": {
"node": true,
"browser": true,
"jest": true,
"jasmine": true
"mocha": true
},
"globals": {
"expect": true
},
"extends": "eslint:recommended"
}
+19
View File
@@ -0,0 +1,19 @@
Thank you for giving us your feedback! We listed some guidelines to help you:
If possible, open separate issues for each bug report. Avoid grouping bugs that are unrelated.
### What is the current behavior?
If the current behavior is a bug, please provide the steps to reproduce. if possible, provide the GitBook version being used (`gitbook -V`) and the system infromations (OS and version).
### What is the expected behavior?
How do you think, or how would you like it should behave.
#### For GitBook.com
Provide, if possible, your username and the URL of the concerned book.
----
The GitBook Team
+3
View File
@@ -1,5 +1,8 @@
sudo: false
language: node_js
os:
- linux
- osx
node_js:
- "stable"
- "4.1"
+36 -2
View File
@@ -2,7 +2,41 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 3.0.0 (pre)
## 3.2.0
- Switch markdown parser from `kramed` to `markup-it`
- Fix support of `|` in tables
- Fix access to `output.name` in templating
- Fix i18n for website and PDF
- Fix minor scrolling issues on website
- Improve options of default search indexer (keywords, disabling per pages)
- Improve pertinence of search results
## 3.1.1
- Fix order of plugins during loading
- Fix error when using math and conrefs
- Fix target attribute for external links
- Fix serve command
## 3.1.0
- Always load themes after plugins
- README/Introduction doesn't require to be the first entry in the summary
- Highlight active entry in summary when scrolling a page with anchors
- Support empty parts in summary
## 3.0.3
- Fix redirection in `gitbook serve` when accessing a folder without trailing slash
- Fix links/annotations for glossary not being correctly resolved
- Fix regression for supporting uppercase filenames in structure files
- Fix `gitbook install` when using Git URLs for plugins
## 3.0.2
- Fix crash for anchor links
## 3.0.1
- Fix regression in link resolution when contain anchor
- Fix `structure.<name>` configuration not supporting filenames with dots
## 3.0.0
- Summary can contain external links and anchors (Fix [#776](https://github.com/GitbookIO/gitbook/issues/776))
- Summary can contain differents entitled sections
- Glossary is generated as a normal page
@@ -11,7 +45,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- `links.sidebar` configuration is no longer supported, use summary sections instead
- `pdf.headerTemplate` and `pdf.footerTemplate` have been replaced by a template in theme/book: `_layout/ebook/pdf_header.html` and `_layout/ebook/pdf_footer.html`
- Markdown parser is now using CommonMark
- Root folder for the book can be specified in a `.gitbook` file
- Root folder for the book can be specified in the `"root"` property of the `book.json` file
- Multi-lingual books share assets folder
- YAML front matter is parsed and can extend page's properties
- Fix `uk` translation
+9
View File
@@ -5,13 +5,18 @@ init:
# Test against these versions of Node.js.
environment:
matrix:
- nodejs_version: "6"
- nodejs_version: "5"
- nodejs_version: "4"
matrix:
fast_finish: true
# Install scripts. (runs after repo cloning)
install:
# Get the latest stable version of Node.js or io.js
- ps: Install-Product node $env:nodejs_version
- set CI=true
# install svgexport
- npm install svgexport -g
# install modules
@@ -27,3 +32,7 @@ test_script:
# Don't actually build.
build: off
# Clone only last commit
shallow_clone: true
clone_depth: 1
+2 -2
View File
@@ -6,10 +6,10 @@ module.exports = {
title: 'GitBook Toolchain Documentation',
// Enforce use of GitBook v3
gitbook: '>=3.0.0-pre.0',
gitbook: '3.1.1',
// Use the "official" theme
plugins: ['theme-official', 'sitemap'],
plugins: ['theme-official@2.1.1', '-sharing', '-fontsettings', 'sitemap'],
variables: {
version: pkg.version
+10 -3
View File
@@ -7,6 +7,7 @@ GitBook allows you to customize your book using a flexible configuration. These
| Variable | Description |
| -------- | ----------- |
| `root` | Path to the root folder containing all the book's files, except `book.json`|
| `structure` | To specify paths for Readme, Summary, Glossary etc. See [Structure paragraph](#structure). |
| `title` | Title of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
| `description` | Description of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
| `author` | Name of the author. On GitBook.com this field is pre-filled. |
@@ -19,18 +20,24 @@ GitBook allows you to customize your book using a flexible configuration. These
Plugins and their configurations are specified in the `book.json`. See [the plugins section](plugins/README.md) for more details.
Since version 3.0.0, GitBook can use themes. See [the theming section](themes/README.md) for more details.
| Variable | Description |
| -------- | ----------- |
| `plugins` | List of plugins to load |
| `pluginsConfig` |Configuration for plugins |
### Theme
### Structure
Since version 3.0.0, GitBook can use themes. See [the theming section](themes/README.md) for more details.
In addition to the `root` variable, you can tell Gitbook the name of the files for Readme, Summary, Glossary, Languages (instead of using the default names such as `README.md`).
These files must be at the root of your book (or the root of every language book). Paths such as `dir/MY_README.md` are not accepted.
| Variable | Description |
| -------- | ----------- |
| `theme` | The theme to use for the book |
| `structure.readme` | Readme file name (defaults to `README.md`) |
| `structure.summary` | Summary file name (defaults to `SUMMARY.md`) |
| `structure.glossary` | Glossary file name (defaults to `GLOSSARY.md`) |
| `structure.languages` | Languages file name (defaults to `LANGS.md`) |
### PDF Options
+4 -3
View File
@@ -29,12 +29,13 @@ You can replace `/usr/bin` with any directory that is in your $PATH.
### Cover
Covers are used for all the ebook formats. It's an important part of an ebook brandline.
Covers are used for all the ebook formats. You can either provide one yourself, or generate one using the [autocover plugin](https://plugins.gitbook.com/plugin/autocover).
To provide a cover, place a **`cover.jpg`** file at the root directory of your book. Adding a **`cover_small.jpg`** will specify a smaller version of the cover. The cover should be a **JPEG** file.
A good cover should respect the following guidelines:
* Size of 1800x2360 (pixels)
* Size of 1800x2360 pixels for `cover.jpg`, 200x262 for `cover_small.jpg`
* No border
* Clearly visible book title
* Any important text should be visible in the small version
+2
View File
@@ -23,5 +23,7 @@ More than 50,000 books have been published on [GitBook.com](https://www.gitbook.
### Documentation
- [DuckDuckHack Documentation](http://docs.duckduckhack.com) by [DuckDuckGo](https://duckduckgo.com/about)
- [Loomio Handbook](http://loomio.coop/) by [Loomio](https://www.loomio.org/)
- [Enspiral Handbook](http://handbook.enspiral.com/) by [Enspiral](http://enspiral.com/)
- This documentation
+3 -3
View File
@@ -2,13 +2,13 @@
Allows you to specify terms and their respective definitions to be displayed as annotations. Based on those terms, GitBook will automatically build an index and highlight those terms in pages.
The `GLOSSARY.md` format is very simple :
The `GLOSSARY.md` format is a list of `h2` headings, along with a description paragraph:
```markdown
# Term
## Term
Definition for this term
# Another term
## Another term
With it's definition, this can contain bold text
and all other kinds of inline markup ...
```
+1 -1
View File
@@ -37,7 +37,7 @@ var filepath = output.resolve('myimage.png');
var fileurl = output.toURL('mychapter/README.md');
// Write a file in the output folder
output.write('hello.txt', 'Hello World')
output.writeFile('hello.txt', 'Hello World')
.then(function() { ... });
// Copy a file to the output folder
+2 -2
View File
@@ -2,7 +2,7 @@
### Testing your plugin locally
Testing your plugin on your book before plushing it is possible using [npm link](https://docs.npmjs.com/cli/link).
Testing your plugin on your book before publishing it is possible using [npm link](https://docs.npmjs.com/cli/link).
In the plugin's folder, run:
@@ -10,7 +10,7 @@ In the plugin's folder, run:
$ npm link
```
The nin your book's folder:
Then in your book's folder:
```
$ npm link gitbook-plugin-<plugin's name>
+1 -1
View File
@@ -14,7 +14,7 @@ It integrates well with the [GitBook Editor](https://www.gitbook.com/editor).
Installing GitBook is easy and straightforward. Your system just needs to meet these two requirements:
* NodeJS (v4.0.0 and above are adviced)
* NodeJS (v4.0.0 and above is recommended)
* Windows, Linux, Unix, or Mac OS X
##### Install with NPM
+4
View File
@@ -0,0 +1,4 @@
/* Reduce size of logo to allow long versions like 3.0.0-pre.1 */
.gb-page-header .container .logo h1 {
font-size: 24px ;
}
+1 -1
View File
@@ -51,5 +51,5 @@ This is the default content
# License
{% import "./LICENSE" %}
{% include "./LICENSE" %}
```
+16 -1
View File
@@ -17,7 +17,22 @@ A theme is a plugin containing templates and assets. Overriding any individual t
### Extend/Customize theme in a book
Authors can extend the templates of a theme directly from their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in
Authors can extend the templates of a theme directly from their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in the installed plugins/themes.
### Extend instead of Forking
When you want to make your theme changes available to multiple books, instead of forking the default theme, you can extend it using the [templating syntax](../templating/README.md):
```html
{% extends template.self %}
{% block body %}
{{ super() }}
... This will be added to the "body" block
{% endblock %}
```
Take a look at the [API](https://github.com/GitbookIO/theme-api) theme for a more complete example.
### Publish a theme
-25
View File
@@ -1,25 +0,0 @@
var path = require('path');
var fs = require('fs');
var matchers = {
/**
Verify that a file exists in a directory
*/
toHaveFile: function () {
return {
compare: function (actual, expected) {
var filePath = path.join(actual, expected);
var exists = fs.existsSync(filePath);
return {
pass: exists
};
}
};
}
};
jasmine.getEnv().beforeEach(function () {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
jasmine.addMatchers(matchers);
});
+1 -1
View File
@@ -3,7 +3,7 @@ var initBook = require('../init');
describe('initBook', function() {
pit('should create a README and SUMMARY for empty book', function() {
it('should create a README and SUMMARY for empty book', function() {
var dir = tmp.dirSync();
return initBook(dir.name)
+1 -3
View File
@@ -1,5 +1,3 @@
var Config = require('../models/config');
/**
Decode changes from a JS API to a config object
@@ -13,7 +11,7 @@ function decodeGlobal(config, result) {
delete values.generator;
delete values.output;
return Config.updateValues(config, values);
return config.updateValues(values);
}
module.exports = decodeGlobal;
+105 -2
View File
@@ -1,14 +1,19 @@
var path = require('path');
var Promise = require('../utils/promise');
var PathUtils = require('../utils/path');
var fs = require('../utils/fs');
var Plugins = require('../plugins');
var deprecate = require('./deprecate');
var encodeConfig = require('./encodeConfig');
var encodeNavigation = require('./encodeNavigation');
var fileToURL = require('../output/helper/fileToURL');
var defaultBlocks = require('../constants/defaultBlocks');
var gitbook = require('../gitbook');
var parsers = require('../parsers');
var encodeConfig = require('./encodeConfig');
var encodeSummary = require('./encodeSummary');
var encodeNavigation = require('./encodeNavigation');
var encodePage = require('./encodePage');
/**
Encode a global context into a JS object
@@ -28,6 +33,7 @@ function encodeGlobal(output) {
var result = {
log: logger,
config: encodeConfig(output, book.getConfig()),
summary: encodeSummary(output, book.getSummary()),
/**
Check if the book is a multilingual book
@@ -67,6 +73,57 @@ function encodeGlobal(output) {
return bookFS.readAsString(fileName);
},
/**
Resolve a file from the book root
@param {String} fileName
@return {String}
*/
resolve: function(fileName) {
return path.resolve(book.getContentRoot(), fileName);
},
/**
Resolve a page by it path
@param {String} filePath
@return {String}
*/
getPageByPath: function(filePath) {
var page = output.getPage(filePath);
if (!page) return undefined;
return encodePage(output, page);
},
/**
Render a block of text (markdown/asciidoc)
@param {String} type
@param {String} text
@return {Promise<String>}
*/
renderBlock: function(type, text) {
var parser = parsers.get(type);
return parser.parsePage(text)
.get('content');
},
/**
Render an inline text (markdown/asciidoc)
@param {String} type
@param {String} text
@return {Promise<String>}
*/
renderInline: function(type, text) {
var parser = parsers.get(type);
return parser.parseInline(text)
.get('content');
},
template: {
/**
Apply a templating block and returns its result
@@ -96,6 +153,16 @@ function encodeGlobal(output) {
return outputFolder;
},
/**
Resolve a file from the output root
@param {String} fileName
@return {String}
*/
resolve: function(fileName) {
return path.resolve(outputFolder, fileName);
},
/**
Convert a filepath into an url
@return {String}
@@ -104,6 +171,21 @@ function encodeGlobal(output) {
return fileToURL(output, filePath);
},
/**
Check that a file exists.
@param {String} fileName
@return {Promise}
*/
hasFile: function(fileName, content) {
return Promise()
.then(function() {
var filePath = PathUtils.resolveInRoot(outputFolder, fileName);
return fs.exists(filePath);
});
},
/**
Write a file to the output folder,
It creates the required folder
@@ -122,6 +204,27 @@ function encodeGlobal(output) {
return fs.writeFile(filePath, content);
});
});
},
/**
Copy a file to the output folder
It creates the required folder.
@param {String} inputFile
@param {String} outputFile
@param {Buffer} content
@return {Promise}
*/
copyFile: function(inputFile, outputFile, content) {
return Promise()
.then(function() {
var outputFilePath = PathUtils.resolveInRoot(outputFolder, outputFile);
return fs.ensureFile(outputFilePath)
.then(function() {
return fs.copy(inputFile, outputFilePath);
});
});
}
},
+51
View File
@@ -0,0 +1,51 @@
var encodeSummaryArticle = require('../json/encodeSummaryArticle');
/**
Encode summary to provide an API to plugin
@param {Output} output
@param {Config} config
@return {Object}
*/
function encodeSummary(output, summary) {
var result = {
/**
Iterate over the summary, it stops when the "iter" returns false
@param {Function} iter
*/
walk: function (iter) {
summary.getArticle(function(article) {
var jsonArticle = encodeSummaryArticle(article, false);
return iter(jsonArticle);
});
},
/**
Get an article by its level
@param {String} level
@return {Object}
*/
getArticleByLevel: function(level) {
var article = summary.getByLevel(level);
return (article? encodeSummaryArticle(article) : undefined);
},
/**
Get an article by its path
@param {String} level
@return {Object}
*/
getArticleByPath: function(level) {
var article = summary.getByPath(level);
return (article? encodeSummaryArticle(article) : undefined);
}
};
return result;
}
module.exports = encodeSummary;
+13 -2
View File
@@ -6,10 +6,21 @@ module.exports = {
// Models
Book: require('./models/book'),
FS: require('./models/fs'),
File: require('./models/file'),
Summary: require('./models/summary'),
Glossary: require('./models/glossary'),
Config: require('./models/config'),
Page: require('./models/page'),
PluginDependency: require('./models/pluginDependency'),
// Modifiers
SummaryModifier: Modifiers.Summary,
ConfigModifier: Modifiers.Config
SummaryModifier: Modifiers.Summary,
ConfigModifier: Modifiers.Config,
// Constants
CONFIG_FILES: require('./constants/configFiles.js'),
IGNORE_FILES: require('./constants/ignoreFiles.js'),
DEFAULT_PLUGINS: require('./constants/defaultPlugins'),
EXTENSIONS_MARKDOWN: require('./constants/extsMarkdown'),
EXTENSIONS_ASCIIDOC: require('./constants/extsAsciidoc')
};
+7 -5
View File
@@ -18,8 +18,10 @@ module.exports = function(format) {
options.log
],
exec: function(args, kwargs) {
var extension = '.' + format;
// Output file will be stored in
var outputFile = args[1] || ('book.' + format);
var outputFile = args[1] || ('book' + extension);
// Create temporary directory
var outputFolder = tmp.dirSync().name;
@@ -42,23 +44,23 @@ module.exports = function(format) {
var languages = book.getLanguages();
if (book.isMultilingual()) {
return Promise.ForEach(languages, function(lang) {
return Promise.forEach(languages.getList(), function(lang) {
var langID = lang.getID();
var langOutputFile = path.join(
path.dirname(outputFile),
path.basename(outputFile, format) + '_' + langID + '.' + format
path.basename(outputFile, extension) + '_' + langID + extension
);
return fs.copy(
path.resolve(outputFolder, langID, 'index.' + format),
path.resolve(outputFolder, langID, 'index' + extension),
langOutputFile
);
})
.thenResolve(languages.getCount());
} else {
return fs.copy(
path.resolve(outputFolder, 'index.' + format),
path.resolve(outputFolder, 'index' + extension),
outputFile
).thenResolve(1);
}
+1 -1
View File
@@ -4,7 +4,7 @@ var options = require('./options');
var initBook = require('../init');
module.exports = {
name: 'install [book]',
name: 'init [book]',
description: 'setup and create files for chapters',
options: [
options.log
+64 -10
View File
@@ -1,6 +1,7 @@
/* eslint-disable no-console */
var tinylr = require('tiny-lr');
var open = require('open');
var Parse = require('../parse');
var Output = require('../output');
@@ -16,11 +17,27 @@ var watch = require('./watch');
var server, lrServer, lrPath;
function waitForCtrlC() {
var d = Promise.defer();
process.on('SIGINT', function() {
d.resolve();
});
return d.promise;
}
function generateBook(args, kwargs) {
var port = kwargs.port;
var outputFolder = getOutputFolder(args);
var book = getBook(args, kwargs);
var Generator = Output.getGenerator(kwargs.format);
var browser = kwargs['browser'];
var hasWatch = kwargs['watch'];
var hasLiveReloading = kwargs['live'];
var hasOpen = kwargs['open'];
// Stop server if running
if (server.isRunning()) console.log('Stopping server');
@@ -29,8 +46,12 @@ function generateBook(args, kwargs) {
.then(function() {
return Parse.parseBook(book)
.then(function(resultBook) {
// Enable livereload plugin
resultBook = ConfigModifier.addPlugin(resultBook, 'livereload');
if (hasLiveReloading) {
// Enable livereload plugin
var config = resultBook.getConfig();
config = ConfigModifier.addPlugin(config, 'livereload');
resultBook = resultBook.set('config', config);
}
return Output.generate(Generator, resultBook, {
root: outputFolder
@@ -45,7 +66,7 @@ function generateBook(args, kwargs) {
.then(function() {
console.log('Serving book on http://localhost:'+port);
if (lrPath) {
if (lrPath && hasLiveReloading) {
// trigger livereload
lrServer.changed({
body: {
@@ -53,9 +74,15 @@ function generateBook(args, kwargs) {
}
});
}
if (hasOpen) {
open('http://localhost:'+port, browser);
}
})
.then(function() {
if (!kwargs.watch) return;
if (!hasWatch) {
return waitForCtrlC();
}
return watch(book.getRoot())
.then(function(filepath) {
@@ -84,21 +111,48 @@ module.exports = {
},
{
name: 'watch',
description: 'Enable/disable file watcher',
description: 'Enable file watcher and live reloading',
defaults: true
},
{
name: 'live',
description: 'Enable live reloading',
defaults: true
},
{
name: 'open',
description: 'Enable opening book in browser',
defaults: false
},
{
name: 'browser',
description: 'Specify browser for opening book',
defaults: ''
},
options.log,
options.format
],
exec: function(args, kwargs) {
server = new Server();
lrServer = tinylr({});
var hasWatch = kwargs['watch'];
var hasLiveReloading = kwargs['live'];
return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
return Promise()
.then(function() {
if (!hasWatch || !hasLiveReloading) {
return;
}
lrServer = tinylr({});
return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
.then(function() {
console.log('Live reload server started on port:', kwargs.lrport);
console.log('Press CTRL+C to quit ...');
console.log('');
});
})
.then(function() {
console.log('Live reload server started on port:', kwargs.lrport);
console.log('Press CTRL+C to quit ...');
console.log('');
return generateBook(args, kwargs);
});
}
+21 -2
View File
@@ -71,11 +71,18 @@ Server.prototype.start = function(dir, port) {
// Redirect to directory's index.html
function redirect() {
var resultURL = urlTransform(req.url, function(parsed) {
parsed.pathname += '/';
return parsed;
});
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
res.setHeader('Location', resultURL);
res.end('Redirecting to ' + resultURL);
}
res.setHeader('X-Current-Location', req.url);
// Send file
send(req, url.parse(req.url).pathname, {
root: dir
@@ -106,4 +113,16 @@ Server.prototype.start = function(dir, port) {
});
};
/**
urlTransform is a helper function that allows a function to transform
a url string in it's parsed form and returns the new url as a string
@param {String} uri
@param {Function} fn
@return {String}
*/
function urlTransform(uri, fn) {
return url.format(fn(url.parse(uri)));
}
module.exports = Server;
+46
View File
@@ -0,0 +1,46 @@
var jsonschema = require('jsonschema');
var schema = require('../configSchema');
describe('configSchema', function() {
function validate(cfg) {
var v = new jsonschema.Validator();
return v.validate(cfg, schema, {
propertyName: 'config'
});
}
describe('structure', function() {
it('should accept dot in filename', function() {
var result = validate({
structure: {
readme: 'book-intro.adoc'
}
});
expect(result.errors.length).toBe(0);
});
it('should accept uppercase in filename', function() {
var result = validate({
structure: {
readme: 'BOOK.adoc'
}
});
expect(result.errors.length).toBe(0);
});
it('should not accept filepath', function() {
var result = validate({
structure: {
readme: 'folder/myFile.md'
}
});
expect(result.errors.length).toBe(1);
});
});
});
+10 -4
View File
@@ -1,3 +1,5 @@
var FILENAME_REGEX = '^[a-zA-Z-._\d,\s]+$';
module.exports = {
'$schema': 'http://json-schema.org/schema#',
'id': 'https://gitbook.com/schemas/book.json',
@@ -16,6 +18,10 @@ module.exports = {
'type': 'string',
'title': 'ISBN for published book'
},
'language': {
'type': 'string',
'title': 'Language of the book'
},
'author': {
'type': 'string',
'title': 'Name of the author'
@@ -57,25 +63,25 @@ module.exports = {
'default': 'LANGS.md',
'type': 'string',
'title': 'File to use as languages index',
'pattern': '^[0-9a-zA-Z ... ]+$'
'pattern': FILENAME_REGEX
},
'readme': {
'default': 'README.md',
'type': 'string',
'title': 'File to use as preface',
'pattern': '^[0-9a-zA-Z ... ]+$'
'pattern': FILENAME_REGEX
},
'glossary': {
'default': 'GLOSSARY.md',
'type': 'string',
'title': 'File to use as glossary index',
'pattern': '^[0-9a-zA-Z ... ]+$'
'pattern': FILENAME_REGEX
},
'summary': {
'default': 'SUMMARY.md',
'type': 'string',
'title': 'File to use as table of contents',
'pattern': '^[0-9a-zA-Z ... ]+$'
'pattern': FILENAME_REGEX
}
},
'additionalProperties': false
+19 -4
View File
@@ -1,9 +1,24 @@
var Immutable = require('immutable');
var PluginDependency = require('../models/pluginDependency');
var pkg = require('../../package.json');
/**
* Create a PluginDependency from a dependency of gitbook
* @param {String} pluginName
* @return {PluginDependency}
*/
function createFromDependency(pluginName) {
var npmID = PluginDependency.nameToNpmID(pluginName);
var version = pkg.dependencies[npmID];
return PluginDependency.create(pluginName, version);
}
/*
List of default plugins for all books,
default plugins should be installed in node dependencies of GitBook
*/
* List of default plugins for all books,
* default plugins should be installed in node dependencies of GitBook
*/
module.exports = Immutable.List([
'highlight',
'search',
@@ -11,4 +26,4 @@ module.exports = Immutable.List([
'sharing',
'fontsettings',
'theme-default'
]);
]).map(createFromDependency);
+4
View File
@@ -0,0 +1,4 @@
module.exports = [
'.adoc',
'.asciidoc'
];
+5
View File
@@ -0,0 +1,5 @@
module.exports = [
'.md',
'.markdown',
'.mdown'
];
+4
View File
@@ -0,0 +1,4 @@
/*
All GitBook themes plugins name start with this prefix once shorted.
*/
module.exports = 'theme-';
+9 -10
View File
@@ -1,7 +1,6 @@
jest.autoMockOff();
var createMockFS = require('../mock');
describe('MockFS', function() {
var createMockFS = require('../mock');
var fs = createMockFS({
'README.md': 'Hello World',
'SUMMARY.md': '# Summary',
@@ -15,35 +14,35 @@ describe('MockFS', function() {
});
describe('exists', function() {
pit('must return true for a file', function() {
it('must return true for a file', function() {
return fs.exists('README.md')
.then(function(result) {
expect(result).toBeTruthy();
});
});
pit('must return false for a non existing file', function() {
it('must return false for a non existing file', function() {
return fs.exists('README_NOTEXISTS.md')
.then(function(result) {
expect(result).toBeFalsy();
});
});
pit('must return true for a directory', function() {
it('must return true for a directory', function() {
return fs.exists('folder')
.then(function(result) {
expect(result).toBeTruthy();
});
});
pit('must return true for a deep file', function() {
it('must return true for a deep file', function() {
return fs.exists('folder/test.md')
.then(function(result) {
expect(result).toBeTruthy();
});
});
pit('must return true for a deep file (2)', function() {
it('must return true for a deep file (2)', function() {
return fs.exists('folder/folder2/hello.md')
.then(function(result) {
expect(result).toBeTruthy();
@@ -52,14 +51,14 @@ describe('MockFS', function() {
});
describe('readAsString', function() {
pit('must return content for a file', function() {
it('must return content for a file', function() {
return fs.readAsString('README.md')
.then(function(result) {
expect(result).toBe('Hello World');
});
});
pit('must return content for a deep file', function() {
it('must return content for a deep file', function() {
return fs.readAsString('folder/test.md')
.then(function(result) {
expect(result).toBe('Cool');
@@ -68,7 +67,7 @@ describe('MockFS', function() {
});
describe('readDir', function() {
pit('must return content for a directory', function() {
it('must return content for a directory', function() {
return fs.readDir('./')
.then(function(files) {
expect(files.size).toBe(3);
+2 -1
View File
@@ -36,6 +36,7 @@ module.exports = function createNodeFS(root) {
fsReadFile: fs.readFile,
fsStatFile: fs.stat,
fsReadDir: fsReadDir,
fsLoadObject: fsLoadObject
fsLoadObject: fsLoadObject,
fsReadAsStream: fs.readStream
});
};
+5 -2
View File
@@ -47,8 +47,11 @@ function initBook(rootFolder) {
// Write pages
return Promise.forEach(articles, function(article) {
var filePath = path.join(rootFolder, article.getPath());
if (!filePath) return;
var articlePath = article.getPath();
var filePath = articlePath? path.join(rootFolder, articlePath) : null;
if (!filePath) {
return;
}
return fs.assertFile(filePath, function() {
return fs.ensureFile(filePath)
+4
View File
@@ -4,6 +4,7 @@ var gitbook = require('../gitbook');
var encodeSummary = require('./encodeSummary');
var encodeGlossary = require('./encodeGlossary');
var encodeReadme = require('./encodeReadme');
var encodeLanguages = require('./encodeLanguages');
/**
Encode a book to JSON
@@ -22,6 +23,9 @@ function encodeBookToJson(book) {
glossary: encodeGlossary(book.getGlossary()),
readme: encodeReadme(book.getReadme()),
config: book.getConfig().getValues().toJS(),
languages: book.isMultilingual()? encodeLanguages(book.getLanguages()) : undefined,
gitbook: {
version: gitbook.version,
time: gitbook.START_TIME
+6 -6
View File
@@ -3,12 +3,12 @@ var encodePage = require('./encodePage');
var encodeFile = require('./encodeFile');
/**
Return a JSON representation of a book with a specific file
@param {Book} output
@param {Page} page
@return {Object}
*/
* Return a JSON representation of a book with a specific file
*
* @param {Book} output
* @param {Page} page
* @return {Object}
*/
function encodeBookWithPage(book, page) {
var file = page.getFile();
+26
View File
@@ -0,0 +1,26 @@
var encodeFile = require('./encodeFile');
/**
Encode a languages listing to JSON
@param {Languages}
@return {Object}
*/
function encodeLanguages(languages) {
var file = languages.getFile();
var list = languages.getList();
return {
file: encodeFile(file),
list: list
.valueSeq()
.map(function(lang) {
return {
id: lang.getID(),
title: lang.getTitle()
};
}).toJS()
};
}
module.exports = encodeLanguages;
+5 -5
View File
@@ -1,11 +1,11 @@
var encodeBook = require('./encodeBook');
/**
Encode an output to JSON
@param {Output}
@return {Object}
*/
* Encode an output to JSON
*
* @param {Output}
* @return {Object}
*/
function encodeOutputToJson(output) {
var book = output.getBook();
var generator = output.getGenerator();
+23
View File
@@ -0,0 +1,23 @@
var encodeOutput = require('./encodeOutput');
var encodePage = require('./encodePage');
var encodeFile = require('./encodeFile');
/**
* Return a JSON representation of a book with a specific file
*
* @param {Book} output
* @param {Page} page
* @return {Object}
*/
function encodeOutputWithPage(output, page) {
var file = page.getFile();
var book = output.getBook();
var result = encodeOutput(output);
result.page = encodePage(page, book.getSummary());
result.file = encodeFile(file);
return result;
}
module.exports = encodeOutputWithPage;
+1
View File
@@ -20,6 +20,7 @@ function encodeSummaryArticle(article, recursive) {
anchor: article.getAnchor(),
url: article.getUrl(),
path: article.getPath(),
ref: article.getRef(),
articles: articles
};
}
+3 -1
View File
@@ -2,10 +2,12 @@
module.exports = {
encodeOutput: require('./encodeOutput'),
encodeBookWithPage: require('./encodeBookWithPage'),
encodeOutputWithPage: require('./encodeOutputWithPage'),
encodeBook: require('./encodeBook'),
encodeFile: require('./encodeFile'),
encodePage: require('./encodePage'),
encodeSummary: require('./encodeSummary'),
encodeSummaryArticle: require('./encodeSummaryArticle'),
encodeReadme: require('./encodeReadme')
encodeReadme: require('./encodeReadme'),
encodeLanguages: require('./encodeLanguages')
};
+31 -4
View File
@@ -1,10 +1,7 @@
jest.autoMockOff();
var Immutable = require('immutable');
var Config = require('../config');
describe('Config', function() {
var Config = require('../config');
var config = Config.createWithValues({
hello: {
world: 1,
@@ -58,6 +55,36 @@ describe('Config', function() {
expect(world).toBe(2);
});
});
describe('toReducedVersion', function() {
it('must only return diffs for simple values', function() {
var _config = Config.createWithValues({
gitbook: '3.0.0'
});
var reducedVersion = _config.toReducedVersion();
expect(reducedVersion.toJS()).toEqual({
gitbook: '3.0.0'
});
});
it('must only return diffs for deep values', function() {
var _config = Config.createWithValues({
structure: {
readme: 'intro.md'
}
});
var reducedVersion = _config.toReducedVersion();
expect(reducedVersion.toJS()).toEqual({
structure: {
readme: 'intro.md'
}
});
});
});
});
+4 -6
View File
@@ -1,10 +1,8 @@
jest.autoMockOff();
var File = require('../file');
var Glossary = require('../glossary');
var GlossaryEntry = require('../glossaryEntry');
describe('Glossary', function() {
var File = require('../file');
var Glossary = require('../glossary');
var GlossaryEntry = require('../glossaryEntry');
var glossary = Glossary.createFromEntries(File(), [
{
name: 'Hello World',
@@ -30,7 +28,7 @@ describe('Glossary', function() {
});
describe('toText', function() {
pit('return as markdown', function() {
it('return as markdown', function() {
return glossary.toText('.md')
.then(function(text) {
expect(text).toContain('# Glossary');
+1 -3
View File
@@ -1,8 +1,6 @@
jest.autoMockOff();
var GlossaryEntry = require('../glossaryEntry');
describe('GlossaryEntry', function() {
var GlossaryEntry = require('../glossaryEntry');
describe('getID', function() {
it('must return a normalized ID', function() {
var entry = new GlossaryEntry({
+28
View File
@@ -0,0 +1,28 @@
var Immutable = require('immutable');
var Page = require('../page');
describe('Page', function() {
describe('toText', function() {
it('must not prepend frontmatter if no attributes', function() {
var page = Page().merge({
content: 'Hello World'
});
expect(page.toText()).toBe('Hello World');
});
it('must prepend frontmatter if attributes', function() {
var page = Page().merge({
content: 'Hello World',
attributes: Immutable.fromJS({
hello: 'world'
})
});
expect(page.toText()).toBe('---\nhello: world\n---\n\nHello World');
});
});
});
+37 -4
View File
@@ -1,6 +1,7 @@
describe('PluginDependency', function() {
var PluginDependency = require('../pluginDependency');
var Immutable = require('immutable');
var PluginDependency = require('../pluginDependency');
describe('PluginDependency', function() {
describe('createFromString', function() {
it('must parse name', function() {
var plugin = PluginDependency.createFromString('hello');
@@ -41,7 +42,39 @@ describe('PluginDependency', function() {
expect(plugin.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
describe('listToArray', function() {
it('must create an array from a list of plugin dependencies', function() {
var list = PluginDependency.listToArray(Immutable.List([
PluginDependency.createFromString('hello@1.0.0'),
PluginDependency.createFromString('noversion'),
PluginDependency.createFromString('-disabled')
]));
expect(list).toEqual([
'hello@1.0.0',
'noversion',
'-disabled'
]);
});
});
describe('listFromArray', function() {
it('must create an array from a list of plugin dependencies', function() {
var arr = Immutable.fromJS([
'hello@1.0.0',
{
'name': 'plugin-ga',
'version': 'git+ssh://samy@github.com/GitbookIO/plugin-ga.git'
}
]);
var list = PluginDependency.listFromArray(arr);
expect(list.first().getName()).toBe('hello');
expect(list.first().getVersion()).toBe('1.0.0');
expect(list.last().getName()).toBe('plugin-ga');
expect(list.last().getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
});
});
+17 -4
View File
@@ -8,11 +8,18 @@ describe('Summary', function() {
articles: [
{
title: 'My First Article',
path: 'README.md'
ref: 'README.md'
},
{
title: 'My Second Article',
path: 'article.md'
ref: 'article.md'
},
{
title: 'Article without ref'
},
{
title: 'Article with absolute ref',
ref: 'https://google.fr'
}
]
},
@@ -33,7 +40,7 @@ describe('Summary', function() {
var part = summary.getByLevel('1');
expect(part).toBeDefined();
expect(part.getArticles().size).toBe(2);
expect(part.getArticles().size).toBe(4);
});
it('can return a Part (2)', function() {
@@ -66,10 +73,16 @@ describe('Summary', function() {
expect(article).toBeDefined();
expect(article.getTitle()).toBe('My Second Article');
});
it('return undefined if not found', function() {
var article = summary.getByPath('NOT_EXISTING.md');
expect(article).toBeFalsy();
});
});
describe('toText', function() {
pit('return as markdown', function() {
it('return as markdown', function() {
return summary.toText('.md')
.then(function(text) {
expect(text).toContain('# Summary');
+53
View File
@@ -0,0 +1,53 @@
var SummaryArticle = require('../summaryArticle');
var File = require('../file');
describe('SummaryArticle', function() {
describe('createChildLevel', function() {
it('must create the right level', function() {
var article = SummaryArticle.create({}, '1.1');
expect(article.createChildLevel()).toBe('1.1.1');
});
it('must create the right level when has articles', function() {
var article = SummaryArticle.create({
articles: [
{
title: 'Test'
}
]
}, '1.1');
expect(article.createChildLevel()).toBe('1.1.2');
});
});
describe('isFile', function() {
it('must return true when exactly the file', function() {
var article = SummaryArticle.create({
ref: 'hello.md'
}, '1.1');
var file = File.createWithFilepath('hello.md');
expect(article.isFile(file)).toBe(true);
});
it('must return true when path is not normalized', function() {
var article = SummaryArticle.create({
ref: '/hello.md'
}, '1.1');
var file = File.createWithFilepath('hello.md');
expect(article.isFile(file)).toBe(true);
});
it('must return false when has anchor', function() {
var article = SummaryArticle.create({
ref: 'hello.md#world'
}, '1.1');
var file = File.createWithFilepath('hello.md');
expect(article.isFile(file)).toBe(false);
});
});
});
+23
View File
@@ -0,0 +1,23 @@
var SummaryPart = require('../summaryPart');
describe('SummaryPart', function() {
describe('createChildLevel', function() {
it('must create the right level', function() {
var article = SummaryPart.create({}, '1');
expect(article.createChildLevel()).toBe('1.1');
});
it('must create the right level when has articles', function() {
var article = SummaryPart.create({
articles: [
{
title: 'Test'
}
]
}, '1');
expect(article.createChildLevel()).toBe('1.2');
});
});
});
+90 -14
View File
@@ -6,18 +6,18 @@ describe('TemplateBlock', function() {
var TemplateBlock = require('../templateBlock');
describe('create', function() {
pit('must initialize a simple TemplateBlock from a function', function() {
it('must initialize a simple TemplateBlock from a function', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<p>Hello, World!</p>';
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
// Check basic templateBlock properties
expect(templateBlock.getName()).toBe('sayhello');
expect(templateBlock.getPost()).toBeNull();
expect(templateBlock.getParse()).toBeTruthy();
expect(templateBlock.getEndTag()).toBe('endsayhello');
expect(templateBlock.getBlocks().size).toBe(0);
expect(templateBlock.getShortcuts().size).toBe(0);
expect(templateBlock.getExtensionName()).toBe('BlocksayhelloExtension');
// Check result of applying block
@@ -32,10 +32,77 @@ describe('TemplateBlock', function() {
});
});
describe('toNunjucksExt()', function() {
pit('must create a valid nunjucks extension', function() {
describe('getShortcuts', function() {
it('must return undefined if no shortcuts', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<p>Hello, World!</p>';
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
expect(templateBlock.getShortcuts()).toNotExist();
});
it('must return complete shortcut', function() {
var templateBlock = TemplateBlock.create('sayhello', {
process: function(block) {
return '<p>Hello, World!</p>';
},
shortcuts: {
parsers: ['markdown'],
start: '$',
end: '-'
}
});
var shortcut = templateBlock.getShortcuts();
expect(shortcut).toBeDefined();
expect(shortcut.getStart()).toEqual('$');
expect(shortcut.getEnd()).toEqual('-');
expect(shortcut.getStartTag()).toEqual('sayhello');
expect(shortcut.getEndTag()).toEqual('endsayhello');
});
});
describe('toNunjucksExt()', function() {
it('should replace by block anchor', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return 'Hello';
});
var blocks = {};
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt({}, blocks);
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello %}{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
blocks = Immutable.fromJS(blocks);
expect(blocks.size).toBe(1);
var blockId = blocks.keySeq().get(0);
var block = blocks.get(blockId);
expect(res).toBe('{{-%' + blockId + '%-}}');
expect(block.get('body')).toBe('Hello');
expect(block.get('name')).toBe('sayhello');
});
});
it('must create a valid nunjucks extension', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
// Create a fresh Nunjucks environment
@@ -53,9 +120,12 @@ describe('TemplateBlock', function() {
});
});
pit('must apply block arguments correctly', function() {
it('must apply block arguments correctly', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<'+block.kwargs.tag+'>Hello, '+block.kwargs.name+'!</'+block.kwargs.tag+'>';
return {
body: '<'+block.kwargs.tag+'>Hello, '+block.kwargs.name+'!</'+block.kwargs.tag+'>',
parse: true
};
});
// Create a fresh Nunjucks environment
@@ -73,11 +143,14 @@ describe('TemplateBlock', function() {
});
});
pit('must accept an async function', function() {
it('must accept an async function', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return Promise()
.then(function() {
return 'Hello ' + block.body;
return {
body: 'Hello ' + block.body,
parse: true
};
});
});
@@ -96,7 +169,7 @@ describe('TemplateBlock', function() {
});
});
pit('must handle nested blocks', function() {
it('must handle nested blocks', function() {
var templateBlock = new TemplateBlock({
name: 'yoda',
blocks: Immutable.List(['start', 'end']),
@@ -107,7 +180,10 @@ describe('TemplateBlock', function() {
nested[blk.name] = blk.body.trim();
});
return '<p class="yoda">'+nested.end+' '+nested.start+'</p>';
return {
body: '<p class="yoda">'+nested.end+' '+nested.start+'</p>',
parse: true
};
}
});
+56 -12
View File
@@ -1,6 +1,5 @@
var path = require('path');
var Immutable = require('immutable');
var Ignore = require('ignore');
var Logger = require('../utils/logger');
@@ -10,7 +9,7 @@ var Readme = require('./readme');
var Summary = require('./summary');
var Glossary = require('./glossary');
var Languages = require('./languages');
var Ignore = require('./ignore');
var Book = Immutable.Record({
// Logger for outptu message
@@ -128,8 +127,7 @@ Book.prototype.isFileIgnored = function(filename) {
filename = path.join(language, filename);
}
return ignore.filter([filename]).length == 0;
return ignore.isFileIgnored(filename);
};
/**
@@ -177,6 +175,17 @@ Book.prototype.isLanguageBook = function() {
return Boolean(this.getLanguage());
};
/**
Return a languages book
@param {String} language
@return {Book}
*/
Book.prototype.getLanguageBook = function(language) {
var books = this.getBooks();
return books.get(language);
};
/**
Add a new language book
@@ -221,6 +230,16 @@ Book.prototype.setConfig = function(config) {
return this.set('config', config);
};
/**
Set the ignore instance for this book
@param {Ignore}
@return {Book}
*/
Book.prototype.setIgnore = function(ignore) {
return this.set('ignore', ignore);
};
/**
Change log level
@@ -274,26 +293,47 @@ Book.prototype.getDefaultExt = function() {
/**
Infer the default path for a Readme.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultReadmePath = function() {
return this.getContentRoot()+'README'+this.getDefaultExt();
Book.prototype.getDefaultReadmePath = function(absolute) {
var defaultPath = 'README'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Infer the default path for a Summary.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultSummaryPath = function() {
return this.getContentRoot()+'SUMMARY'+this.getDefaultExt();
Book.prototype.getDefaultSummaryPath = function(absolute) {
var defaultPath = 'SUMMARY'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Infer the default path for a Glossary.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultGlossaryPath = function() {
return this.getContentRoot()+'GLOSSARY'+this.getDefaultExt();
Book.prototype.getDefaultGlossaryPath = function(absolute) {
var defaultPath = 'GLOSSARY'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
@@ -305,12 +345,16 @@ Book.prototype.getDefaultGlossaryPath = function() {
*/
Book.createFromParent = function createFromParent(parent, language) {
var ignore = parent.getIgnore();
var config = parent.getConfig();
// Set language in configuration
config = config.setValue('language', language);
return new Book({
// Inherits config. logegr and list of ignored files
logger: parent.getLogger(),
config: parent.getConfig(),
ignore: Ignore().add(ignore),
config: config,
ignore: ignore,
language: language,
fs: FS.reduceScope(parent.getContentFS(), language)
+95 -48
View File
@@ -4,6 +4,7 @@ var Immutable = require('immutable');
var File = require('./file');
var PluginDependency = require('./pluginDependency');
var configDefault = require('../constants/configDefault');
var reducedObject = require('../utils/reducedObject');
var Config = Immutable.Record({
file: File(),
@@ -19,11 +20,36 @@ Config.prototype.getValues = function() {
};
/**
Return a configuration value by its key path
* Return minimum version of configuration,
* Basically it returns the current config minus the default one
* @return {Map}
*/
Config.prototype.toReducedVersion = function() {
return reducedObject(configDefault, this.getValues());
};
@param {String} key
@return {Mixed}
*/
/**
* Render config as text
* @return {Promise<String>}
*/
Config.prototype.toText = function() {
return JSON.stringify(this.toReducedVersion().toJS(), null, 4);
};
/**
* Change the file for the configuration
* @param {File} file
* @return {Config}
*/
Config.prototype.setFile = function(file) {
return this.set('file', file);
};
/**
* Return a configuration value by its key path
* @param {String} key
* @return {Mixed}
*/
Config.prototype.getValue = function(keyPath, def) {
var values = this.getValues();
keyPath = Config.keyToKeyPath(keyPath);
@@ -36,12 +62,11 @@ Config.prototype.getValue = function(keyPath, def) {
};
/**
Update a configuration value
@param {String} key
@param {Mixed} value
@return {Mixed}
*/
* Update a configuration value
* @param {String} key
* @param {Mixed} value
* @return {Config}
*/
Config.prototype.setValue = function(keyPath, value) {
keyPath = Config.keyToKeyPath(keyPath);
@@ -54,10 +79,9 @@ Config.prototype.setValue = function(keyPath, value) {
};
/**
Return a list of plugin dependencies
@return {List<PluginDependency>}
*/
* Return a list of plugin dependencies
* @return {List<PluginDependency>}
*/
Config.prototype.getPluginDependencies = function() {
var plugins = this.getValue('plugins');
@@ -69,24 +93,62 @@ Config.prototype.getPluginDependencies = function() {
};
/**
Update the list of plugins dependencies
* Return a plugin dependency by its name
* @param {String} name
* @return {PluginDependency}
*/
Config.prototype.getPluginDependency = function(name) {
var plugins = this.getPluginDependencies();
@param {List<PluginDependency>}
@return {Config}
*/
return plugins.find(function(dep) {
return dep.getName() === name;
});
};
/**
* Update the list of plugins dependencies
* @param {List<PluginDependency>}
* @return {Config}
*/
Config.prototype.setPluginDependencies = function(deps) {
var plugins = PluginDependency.listFromArray(deps);
var plugins = PluginDependency.listToArray(deps);
return this.setValue('plugins', plugins);
};
/**
Create a new config for a file
@param {File} file
@param {Object} values
@returns {Config}
*/
/**
* Update values for an existing configuration
* @param {Object} values
* @returns {Config}
*/
Config.prototype.updateValues = function(values) {
values = Immutable.fromJS(values);
return this.set('values', values);
};
/**
* Update values for an existing configuration
* @param {Config} config
* @param {Object} values
* @returns {Config}
*/
Config.prototype.mergeValues = function(values) {
var currentValues = this.getValues();
values = Immutable.fromJS(values);
currentValues = currentValues.mergeDeep(values);
return this.set('values', currentValues);
};
/**
* Create a new config for a file
* @param {File} file
* @param {Object} values
* @returns {Config}
*/
Config.create = function(file, values) {
return new Config({
file: file,
@@ -95,37 +157,22 @@ Config.create = function(file, values) {
};
/**
Create a new config
@param {Object} values
@returns {Config}
*/
* Create a new config
* @param {Object} values
* @returns {Config}
*/
Config.createWithValues = function(values) {
return new Config({
values: Immutable.fromJS(values)
});
};
/**
Update values for an existing configuration
@param {Config} config
@param {Object} values
@returns {Config}
*/
Config.updateValues = function(config, values) {
values = Immutable.fromJS(values);
return config.set('values', values);
};
/**
Convert a keyPath to an array of keys
@param {String|Array}
@return {Array}
*/
* Convert a keyPath to an array of keys
* @param {String|Array}
* @return {Array}
*/
Config.keyToKeyPath = function(keyPath) {
if (is.string(keyPath)) keyPath = keyPath.split('.');
return keyPath;
+42 -9
View File
@@ -1,5 +1,6 @@
var path = require('path');
var Immutable = require('immutable');
var stream = require('stream');
var File = require('./file');
var Promise = require('../utils/promise');
@@ -13,7 +14,9 @@ var FS = Immutable.Record({
fsReadFile: Function(),
fsStatFile: Function(),
fsReadDir: Function(),
fsLoadObject: null
fsLoadObject: null,
fsReadAsStream: null
});
/**
@@ -111,6 +114,30 @@ FS.prototype.readAsString = function(filename, encoding) {
});
};
/**
Read file as a stream
@param {String} filename
@return {Promise<Stream>}
*/
FS.prototype.readAsStream = function(filename) {
var that = this;
var filepath = that.resolve(filename);
var fsReadAsStream = this.get('fsReadAsStream');
if (fsReadAsStream) {
return Promise(fsReadAsStream(filepath));
}
return this.read(filename)
.then(function(buf) {
var bufferStream = new stream.PassThrough();
bufferStream.end(buf);
return bufferStream;
});
};
/**
Read stat infos about a file
@@ -171,23 +198,29 @@ FS.prototype.listFiles = function(dirname) {
/**
List all files in a directory
@param {String} dirname
@param {String} dirName
@param {Function(dirName)} filterFn: call it for each file/directory to test if it should stop iterating
@return {Promise<List<String>>}
*/
FS.prototype.listAllFiles = function(folder) {
FS.prototype.listAllFiles = function(dirName, filterFn) {
var that = this;
folder = folder || '.';
dirName = dirName || '.';
return this.readDir(folder)
return this.readDir(dirName)
.then(function(files) {
return Promise.reduce(files, function(out, file) {
var isDirectory = pathIsFolder(file);
var newDirName = path.join(dirName, file);
if (!isDirectory) {
return out.push(path.join(folder, file));
if (filterFn && filterFn(newDirName) === false) {
return out;
}
return that.listAllFiles(path.join(folder, file))
if (!isDirectory) {
return out.push(newDirName);
}
return that.listAllFiles(newDirName, filterFn)
.then(function(inner) {
return out.concat(inner);
});
@@ -196,7 +229,7 @@ FS.prototype.listAllFiles = function(folder) {
};
/**
Find a file in a folder (case incensitive)
Find a file in a folder (case insensitive)
Return the found filename
@param {String} dirname
+42
View File
@@ -0,0 +1,42 @@
var Immutable = require('immutable');
var IgnoreMutable = require('ignore');
/*
Immutable version of node-ignore
*/
var Ignore = Immutable.Record({
ignore: new IgnoreMutable()
}, 'Ignore');
Ignore.prototype.getIgnore = function() {
return this.get('ignore');
};
/**
Test if a file is ignored by these rules
@param {String} filePath
@return {Boolean}
*/
Ignore.prototype.isFileIgnored = function(filename) {
var ignore = this.getIgnore();
return ignore.filter([filename]).length == 0;
};
/**
Add rules
@param {String}
@return {Ignore}
*/
Ignore.prototype.add = function(rule) {
var ignore = this.getIgnore();
var newIgnore = new IgnoreMutable();
newIgnore.add(ignore);
newIgnore.add(rule);
return this.set('ignore', newIgnore);
};
module.exports = Ignore;
+14
View File
@@ -1,6 +1,7 @@
var Immutable = require('immutable');
var Book = require('./book');
var LocationUtils = require('../utils/location');
var Output = Immutable.Record({
book: Book(),
@@ -52,6 +53,19 @@ Output.prototype.getState = function() {
return this.get('state');
};
/**
Return a page byt its file path
@param {String} filePath
@return {Page|undefined}
*/
Output.prototype.getPage = function(filePath) {
filePath = LocationUtils.normalize(filePath);
var pages = this.getPages();
return pages.get(filePath);
};
/**
Get root folder for output
+21 -6
View File
@@ -1,4 +1,5 @@
var Immutable = require('immutable');
var yaml = require('js-yaml');
var File = require('./file');
@@ -32,19 +33,33 @@ Page.prototype.getDir = function() {
};
/**
Return path of the page
* Return page as text
* @return {String}
*/
Page.prototype.toText = function() {
var attrs = this.getAttributes();
var content = this.getContent();
@return {String}
if (attrs.size === 0) {
return content;
}
var frontMatter = '---\n' + yaml.safeDump(attrs.toJS(), { skipInvalid: true }) + '---\n\n';
return (frontMatter + content);
};
/**
* Return path of the page
* @return {String}
*/
Page.prototype.getPath = function() {
return this.getFile().getPath();
};
/**
Create a page for a file
@param {File} file
@return {Page}
* Create a page for a file
* @param {File} file
* @return {Page}
*/
Page.createForFile = function(file) {
return new Page({
+9
View File
@@ -43,6 +43,10 @@ Parser.prototype.parseGlossary = function(content) {
Parser.prototype.preparePage = function(content) {
var page = this.get('page');
if (!page.prepare) {
return Promise(content);
}
return Promise(page.prepare(content));
};
@@ -51,6 +55,11 @@ Parser.prototype.parsePage = function(content) {
return Promise(page(content));
};
Parser.prototype.parseInline = function(content) {
var inline = this.get('inline');
return Promise(inline(content));
};
Parser.prototype.parseLanguages = function(content) {
var langs = this.get('langs');
return Promise(langs(content));
+53 -49
View File
@@ -1,7 +1,9 @@
var Immutable = require('immutable');
var TemplateBlock = require('./templateBlock');
var PREFIX = require('../constants/pluginPrefix');
var PluginDependency = require('./pluginDependency');
var THEME_PREFIX = require('../constants/themePrefix');
var DEFAULT_VERSION = '*';
var Plugin = Immutable.Record({
@@ -16,6 +18,9 @@ var Plugin = Immutable.Record({
// Depth of this plugin in the dependency tree
depth: Number(0),
// Parent depending on this plugin
parent: String(),
// Content of the "package.json"
package: Immutable.Map(),
@@ -47,38 +52,48 @@ Plugin.prototype.getDepth = function() {
return this.get('depth');
};
/**
Return the ID on NPM for this plugin
@return {String}
*/
Plugin.prototype.getNpmID = function() {
return Plugin.nameToNpmID(this.getName());
Plugin.prototype.getParent = function() {
return this.get('parent');
};
/**
Check if a plugin is loaded
* Return the ID on NPM for this plugin
* @return {String}
*/
Plugin.prototype.getNpmID = function() {
return PluginDependency.nameToNpmID(this.getName());
};
@return {Boolean}
*/
/**
* Check if a plugin is loaded
* @return {Boolean}
*/
Plugin.prototype.isLoaded = function() {
return Boolean(this.getPackage().size > 0);
};
/**
Return map of hooks
@return {Map<String:Function>}
*/
* Check if a plugin is a theme given its name
* @return {Boolean}
*/
Plugin.prototype.isTheme = function() {
var name = this.getName();
return (name && name.indexOf(THEME_PREFIX) === 0);
};
/**
* Return map of hooks
* @return {Map<String:Function>}
*/
Plugin.prototype.getHooks = function() {
return this.getContent().get('hooks') || Immutable.Map();
};
/**
Return infos about resources for a specific type
@param {String} type
@return {Map<String:Mixed>}
*/
* Return infos about resources for a specific type
* @param {String} type
* @return {Map<String:Mixed>}
*/
Plugin.prototype.getResources = function(type) {
if (type != 'website' && type != 'ebook') {
throw new Error('Invalid assets type ' + type);
@@ -91,17 +106,17 @@ Plugin.prototype.getResources = function(type) {
};
/**
Return map of filters
@return {Map<String:Function>}
*/
* Return map of filters
* @return {Map<String:Function>}
*/
Plugin.prototype.getFilters = function() {
return this.getContent().get('filters');
};
/**
Return map of blocks
@return {Map<String:TemplateBlock>}
*/
* Return map of blocks
* @return {Map<String:TemplateBlock>}
*/
Plugin.prototype.getBlocks = function() {
var blocks = this.getContent().get('blocks');
blocks = blocks || Immutable.Map();
@@ -113,21 +128,19 @@ Plugin.prototype.getBlocks = function() {
};
/**
Return a specific hook
@param {String} name
@return {Function|undefined}
*/
* Return a specific hook
* @param {String} name
* @return {Function|undefined}
*/
Plugin.prototype.getHook = function(name) {
return this.getHooks().get(name);
};
/**
Create a plugin from a string
@param {String}
@return {Plugin}
*/
* Create a plugin from a string
* @param {String}
* @return {Plugin}
*/
Plugin.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
@@ -140,11 +153,10 @@ Plugin.createFromString = function(s) {
};
/**
Create a plugin from a dependency
@param {PluginDependency}
@return {Plugin}
*/
* Create a plugin from a dependency
* @param {PluginDependency}
* @return {Plugin}
*/
Plugin.createFromDep = function(dep) {
return new Plugin({
name: dep.getName(),
@@ -152,14 +164,6 @@ Plugin.createFromDep = function(dep) {
});
};
/**
Return NPM id for a plugin name
@param {String}
@return {String}
*/
Plugin.nameToNpmID = function(s) {
return PREFIX + s;
};
Plugin.nameToNpmID = PluginDependency.nameToNpmID;
module.exports = Plugin;
+81 -28
View File
@@ -1,12 +1,14 @@
var is = require('is');
var semver = require('semver');
var Immutable = require('immutable');
var PREFIX = require('../constants/pluginPrefix');
var DEFAULT_VERSION = '*';
/*
PluginDependency represents the informations about a plugin
stored in config.plugins
*/
* PluginDependency represents the informations about a plugin
* stored in config.plugins
*/
var PluginDependency = Immutable.Record({
name: String(),
@@ -30,11 +32,56 @@ PluginDependency.prototype.isEnabled = function() {
};
/**
Create a plugin from a string
* Toggle this plugin state
* @param {Boolean}
* @return {PluginDependency}
*/
PluginDependency.prototype.toggle = function(state) {
if (is.undef(state)) {
state = !this.isEnabled();
}
@param {String}
@return {Plugin|undefined}
*/
return this.set('enabled', state);
};
/**
* Return NPM ID for the dependency
* @return {String}
*/
PluginDependency.prototype.getNpmID = function() {
return PluginDependency.nameToNpmID(this.getName());
};
/**
* Is the plugin using a git dependency
* @return {Boolean}
*/
PluginDependency.prototype.isGitDependency = function() {
return !semver.validRange(this.getVersion());
};
/**
* Create a plugin with a name and a plugin
* @param {String}
* @return {Plugin|undefined}
*/
PluginDependency.create = function(name, version, enabled) {
if (is.undefined(enabled)) {
enabled = true;
}
return new PluginDependency({
name: name,
version: version || DEFAULT_VERSION,
enabled: Boolean(enabled)
});
};
/**
* Create a plugin from a string
* @param {String}
* @return {Plugin|undefined}
*/
PluginDependency.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
@@ -54,22 +101,20 @@ PluginDependency.createFromString = function(s) {
};
/**
Create a PluginDependency from a string
@param {String}
@return {List<PluginDependency>}
*/
* Create a PluginDependency from a string
* @param {String}
* @return {List<PluginDependency>}
*/
PluginDependency.listFromString = function(s) {
var parts = s.split(',');
return PluginDependency.listFromArray(parts);
};
/**
Create a PluginDependency from an array
@param {Array}
@return {List<PluginDependency>}
*/
* Create a PluginDependency from an array
* @param {Array}
* @return {List<PluginDependency>}
*/
PluginDependency.listFromArray = function(arr) {
return Immutable.List(arr)
.map(function(entry) {
@@ -77,8 +122,8 @@ PluginDependency.listFromArray = function(arr) {
return PluginDependency.createFromString(entry);
} else {
return PluginDependency({
name: entry.name,
version: entry.version
name: entry.get('name'),
version: entry.get('version')
});
}
})
@@ -88,17 +133,16 @@ PluginDependency.listFromArray = function(arr) {
};
/**
Export plugin dependencies as an array
@param {List<PluginDependency>}
@return {Array<String>}
*/
PluginDependency.listToArray = function(arr) {
return arr
* Export plugin dependencies as an array
* @param {List<PluginDependency>} list
* @return {Array<String>}
*/
PluginDependency.listToArray = function(list) {
return list
.map(function(dep) {
var result;
var result = '';
if (dep.isEnabled()) {
if (!dep.isEnabled()) {
result += '-';
}
@@ -112,4 +156,13 @@ PluginDependency.listToArray = function(arr) {
.toJS();
};
/**
* Return NPM id for a plugin name
* @param {String}
* @return {String}
*/
PluginDependency.nameToNpmID = function(s) {
return PREFIX + s;
};
module.exports = PluginDependency;
+39 -2
View File
@@ -56,7 +56,7 @@ Summary.prototype.getArticle = function(iter, partIter) {
Return a part/article by its level
@param {String} level
@return {Article}
@return {Article|Part}
*/
Summary.prototype.getByLevel = function(level) {
function iterByLevel(article) {
@@ -74,7 +74,12 @@ Summary.prototype.getByLevel = function(level) {
*/
Summary.prototype.getByPath = function(filePath) {
return this.getArticle(function(article) {
return (LocationUtils.areIdenticalPaths(article.getPath(), filePath));
var articlePath = article.getPath();
return (
articlePath &&
LocationUtils.areIdenticalPaths(articlePath, filePath)
);
});
};
@@ -129,6 +134,27 @@ Summary.prototype.getPrevArticle = function(current) {
return prev;
};
/**
Return the parent article, or parent part of an article
@param {String|Article} current
@return {Article|Part|Null}
*/
Summary.prototype.getParent = function (level) {
// Coerce to level
level = is.string(level)? level : level.getLevel();
// Get parent level
var parentLevel = getParentLevel(level);
if (!parentLevel) {
return null;
}
// Get parent of the position
var parentArticle = this.getByLevel(parentLevel);
return parentArticle || null;
};
/**
Render summary as text
@@ -188,4 +214,15 @@ Summary.createFromParts = function createFromParts(file, parts) {
});
};
/**
Returns parent level of a level
@param {String} level
@return {String}
*/
function getParentLevel(level) {
var parts = level.split('.');
return parts.slice(0, -1).join('.');
}
module.exports = Summary;
+77 -38
View File
@@ -30,19 +30,21 @@ SummaryArticle.prototype.getArticles = function() {
};
/**
Return how deep the article is
@return {Number}
*/
* Return how deep the article is.
* The README has a depth of 1
*
* @return {Number}
*/
SummaryArticle.prototype.getDepth = function() {
return this.getLevel().split('.').length;
return (this.getLevel().split('.').length - 1);
};
/**
Get path (without anchor) to the pointing file
@return {String}
*/
* Get path (without anchor) to the pointing file.
* It also normalizes the file path.
*
* @return {String}
*/
SummaryArticle.prototype.getPath = function() {
if (this.isExternal()) {
return undefined;
@@ -53,29 +55,28 @@ SummaryArticle.prototype.getPath = function() {
return undefined;
}
var parts = ref.split('#');
var pathname = (parts.length > 1? parts.slice(0, -1).join('#') : ref);
// Normalize path to remove ('./', etc)
return location.normalize(pathname);
// Normalize path to remove ('./', '/...', etc)
return location.flatten(pathname);
};
/**
Return url if article is external
@return {String}
*/
* Return url if article is external
*
* @return {String}
*/
SummaryArticle.prototype.getUrl = function() {
return this.isExternal()? this.getRef() : undefined;
};
/**
Get anchor for this article (or undefined)
@return {String}
*/
* Get anchor for this article (or undefined)
*
* @return {String}
*/
SummaryArticle.prototype.getAnchor = function() {
var ref = this.getRef();
var parts = ref.split('#');
@@ -85,29 +86,68 @@ SummaryArticle.prototype.getAnchor = function() {
};
/**
Is article pointing to a page of an absolute url
* Create a new level for a new child article
*
* @return {String}
*/
SummaryArticle.prototype.createChildLevel = function() {
var level = this.getLevel();
var subArticles = this.getArticles();
var childLevel = level + '.' + (subArticles.size + 1);
@return {Boolean}
*/
return childLevel;
};
/**
* Is article pointing to a page of an absolute url
*
* @return {Boolean}
*/
SummaryArticle.prototype.isPage = function() {
return !this.isExternal() && this.getRef();
};
/**
Is article pointing to aan absolute url
* Check if this article is a file (exatcly)
*
* @param {File} file
* @return {Boolean}
*/
SummaryArticle.prototype.isFile = function(file) {
return (
file.getPath() === this.getPath()
&& this.getAnchor() === undefined
);
};
@return {Boolean}
*/
/**
* Check if this article is the introduction of the book
*
* @param {Book|Readme} book
* @return {Boolean}
*/
SummaryArticle.prototype.isReadme = function(book) {
var readme = book.getFile? book : book.getReadme();
var file = readme.getFile();
return this.isFile(file);
};
/**
* Is article pointing to aan absolute url
*
* @return {Boolean}
*/
SummaryArticle.prototype.isExternal = function() {
return location.isExternal(this.getRef());
};
/**
Create a SummaryArticle
@param {Object} def
@return {SummaryArticle}
*/
* Create a SummaryArticle
*
* @param {Object} def
* @return {SummaryArticle}
*/
SummaryArticle.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) {
@@ -124,14 +164,13 @@ SummaryArticle.create = function(def, level) {
});
};
/**
Find an article from a base one
@param {Article|Part} base
@param {Function(article)} iter
@return {Article}
*/
* Find an article from a base one
*
* @param {Article|Part} base
* @param {Function(article)} iter
* @return {Article}
*/
SummaryArticle.findArticle = function(base, iter) {
var articles = base.getArticles();
+17 -4
View File
@@ -25,11 +25,24 @@ SummaryPart.prototype.getArticles = function() {
};
/**
Create a SummaryPart
* Create a new level for a new child article
*
* @return {String}
*/
SummaryPart.prototype.createChildLevel = function() {
var level = this.getLevel();
var subArticles = this.getArticles();
var childLevel = level + '.' + (subArticles.size + 1);
@param {Object} def
@return {SummaryPart}
*/
return childLevel;
};
/**
* Create a SummaryPart
*
* @param {Object} def
* @return {SummaryPart}
*/
SummaryPart.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) {
+55 -84
View File
@@ -4,11 +4,10 @@ var Immutable = require('immutable');
var Promise = require('../utils/promise');
var genKey = require('../utils/genKey');
var TemplateShortcut = require('./templateShortcut');
var NODE_ENDARGS = '%%endargs%%';
var blockBodies = {};
var TemplateBlock = Immutable.Record({
// Name of block, also the start tag
name: String(),
@@ -23,26 +22,13 @@ var TemplateBlock = Immutable.Record({
blocks: Immutable.List(),
// List of shortcuts to replace with this block
shortcuts: Immutable.List(),
// Function to execute in post processing
post: null,
parse: true
shortcuts: Immutable.Map()
}, 'TemplateBlock');
TemplateBlock.prototype.getName = function() {
return this.get('name');
};
TemplateBlock.prototype.getPost = function() {
return this.get('post');
};
TemplateBlock.prototype.getParse = function() {
return this.get('parse');
};
TemplateBlock.prototype.getEndTag = function() {
return this.get('end') || ('end' + this.getName());
};
@@ -55,25 +41,35 @@ TemplateBlock.prototype.getBlocks = function() {
return this.get('blocks');
};
/**
* Return shortcuts associated with this block or undefined
* @return {TemplateShortcut|undefined}
*/
TemplateBlock.prototype.getShortcuts = function() {
return this.get('shortcuts');
var shortcuts = this.get('shortcuts');
if (shortcuts.size === 0) {
return undefined;
}
return TemplateShortcut.createForBlock(this, shortcuts);
};
/**
Return name for the nunjucks extension
@return {String}
*/
* Return name for the nunjucks extension
* @return {String}
*/
TemplateBlock.prototype.getExtensionName = function() {
return 'Block' + this.getName() + 'Extension';
};
/**
Return a nunjucks extension to represents this block
* Return a nunjucks extension to represents this block
* @return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext, blocksOutput) {
blocksOutput = blocksOutput || {};
@return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
var that = this;
var name = this.getName();
var endTag = this.getEndTag();
@@ -183,7 +179,7 @@ TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
return that.applyBlock(mainBlock, ctx);
})
.then(function(result) {
return that.blockResultToHtml(result);
return that.blockResultToHtml(result, blocksOutput);
})
.nodeify(callback);
};
@@ -193,11 +189,11 @@ TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
};
/**
Apply a block to a content
@param {Object} inner
@param {Object} context
@return {Promise<String>|String}
*/
* Apply a block to a content
* @param {Object} inner
* @param {Object} context
* @return {Promise<String>|String}
*/
TemplateBlock.prototype.applyBlock = function(inner, context) {
var processFn = this.getProcess();
@@ -209,19 +205,18 @@ TemplateBlock.prototype.applyBlock = function(inner, context) {
var r = processFn.call(context, inner);
if (Promise.isPromiseAlike(r)) {
return r.then(this.handleBlockResult.bind(this));
return r.then(this.normalizeBlockResult.bind(this));
} else {
return this.handleBlockResult(r);
return this.normalizeBlockResult(r);
}
};
/**
Handle result from a block process function
@param {Object} result
@return {Object}
*/
TemplateBlock.prototype.handleBlockResult = function(result) {
* Normalize result from a block process function
* @param {Object|String} result
* @return {Object}
*/
TemplateBlock.prototype.normalizeBlockResult = function(result) {
if (is.string(result)) {
result = { body: result };
}
@@ -231,22 +226,22 @@ TemplateBlock.prototype.handleBlockResult = function(result) {
};
/**
Convert a block result to HTML
@param {Object} result
@return {String}
*/
TemplateBlock.prototype.blockResultToHtml = function(result) {
var parse = this.getParse();
* Convert a block result to HTML
* @param {Object} result
* @param {Object} blocksOutput: stored post processing blocks in this object
* @return {String}
*/
TemplateBlock.prototype.blockResultToHtml = function(result, blocksOutput) {
var indexedKey;
var toIndex = (!parse) || (this.getPost() !== undefined);
var toIndex = (!result.parse) || (result.post !== undefined);
if (toIndex) {
indexedKey = TemplateBlock.indexBlockResult(result);
indexedKey = genKey();
blocksOutput[indexedKey] = result;
}
// Parsable block, just return it
if (parse) {
if (result.parse) {
return result.body;
}
@@ -256,35 +251,11 @@ TemplateBlock.prototype.blockResultToHtml = function(result) {
};
/**
Index a block result, and return the indexed key
@param {Object} blk
@return {String}
*/
TemplateBlock.indexBlockResult = function(blk) {
var key = genKey();
blockBodies[key] = blk;
return key;
};
/**
Get a block results indexed for a specific key
@param {String} key
@return {Object|undefined}
*/
TemplateBlock.getBlockResultByKey = function(key) {
return blockBodies[key];
};
/**
Create a template block from a function or an object
@param {String} blockName
@param {Object} block
@return {TemplateBlock}
*/
* Create a template block from a function or an object
* @param {String} blockName
* @param {Object} block
* @return {TemplateBlock}
*/
TemplateBlock.create = function(blockName, block) {
if (is.fn(block)) {
block = new Immutable.Map({
@@ -292,16 +263,16 @@ TemplateBlock.create = function(blockName, block) {
});
}
block = new TemplateBlock(block);
block = block.set('name', blockName);
return new TemplateBlock(block);
return block;
};
/**
Extract kwargs from an arguments array
@param {Array} args
@return {Object}
*/
* Extract kwargs from an arguments array
* @param {Array} args
* @return {Object}
*/
function extractKwargs(args) {
var last = args[args.length - 1];
return (is.object(last) && last.__keywords)? args.pop() : {};
+2 -2
View File
@@ -67,7 +67,7 @@ TemplateEngine.prototype.getBlock = function(name) {
@return {Nunjucks.Environment}
*/
TemplateEngine.prototype.toNunjucks = function() {
TemplateEngine.prototype.toNunjucks = function(blocksOutput) {
var loader = this.getLoader();
var blocks = this.getBlocks();
var filters = this.getFilters();
@@ -101,7 +101,7 @@ TemplateEngine.prototype.toNunjucks = function() {
// Add blocks
blocks.forEach(function(block) {
var extName = block.getExtensionName();
var Ext = block.toNunjucksExt(context);
var Ext = block.toNunjucksExt(context, blocksOutput);
env.addExtension(extName, new Ext());
});
+42
View File
@@ -0,0 +1,42 @@
var Immutable = require('immutable');
var TemplateOutput = Immutable.Record({
// Text content of the template
content: String(),
// Map of blocks to replace / post process
blocks: Immutable.Map()
}, 'TemplateOutput');
TemplateOutput.prototype.getContent = function() {
return this.get('content');
};
TemplateOutput.prototype.getBlocks = function() {
return this.get('blocks');
};
/**
* Update content of this output
* @param {String} content
* @return {TemplateContent}
*/
TemplateOutput.prototype.setContent = function(content) {
return this.set('content', content);
};
/**
* Create a TemplateOutput from a text content
* and an object containing block definition
* @param {String} content
* @param {Object} blocks
* @return {TemplateOutput}
*/
TemplateOutput.create = function(content, blocks) {
return new TemplateOutput({
content: content,
blocks: Immutable.fromJS(blocks)
});
};
module.exports = TemplateOutput;
+73
View File
@@ -0,0 +1,73 @@
var Immutable = require('immutable');
var is = require('is');
/*
A TemplateShortcut is defined in plugin's template blocks
to replace content with a templating block using delimiters.
*/
var TemplateShortcut = Immutable.Record({
// List of parser names accepting this shortcut
parsers: Immutable.Map(),
start: String(),
end: String(),
startTag: String(),
endTag: String()
}, 'TemplateShortcut');
TemplateShortcut.prototype.getStart = function() {
return this.get('start');
};
TemplateShortcut.prototype.getEnd = function() {
return this.get('end');
};
TemplateShortcut.prototype.getStartTag = function() {
return this.get('startTag');
};
TemplateShortcut.prototype.getEndTag = function() {
return this.get('endTag');
};
TemplateShortcut.prototype.getParsers = function() {
return this.get('parsers');
};
/**
Test if this shortcut accept a parser
@param {Parser|String} parser
@return {Boolean}
*/
TemplateShortcut.prototype.acceptParser = function(parser) {
if (!is.string(parser)) {
parser = parser.getName();
}
var parserNames = this.get('parsers');
return parserNames.includes(parser);
};
/**
Create a shortcut for a block
@param {TemplateBlock} block
@param {Map} details
@return {TemplateShortcut}
*/
TemplateShortcut.createForBlock = function(block, details) {
details = Immutable.fromJS(details);
return new TemplateShortcut({
parsers: details.get('parsers'),
start: details.get('start'),
end: details.get('end'),
startTag: block.getName(),
endTag: block.getEndTag()
});
};
module.exports = TemplateShortcut;
@@ -0,0 +1,32 @@
var addPlugin = require('../addPlugin');
var Config = require('../../../models/config');
describe('addPlugin', function() {
var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled']
});
it('should have correct state of dependencies', function() {
var disabledDep = config.getPluginDependency('disabled');
expect(disabledDep).toBeDefined();
expect(disabledDep.getVersion()).toEqual('*');
expect(disabledDep.isEnabled()).toBeFalsy();
});
it('should add the plugin to the list', function() {
var newConfig = addPlugin(config, 'test');
var testDep = newConfig.getPluginDependency('test');
expect(testDep).toBeDefined();
expect(testDep.getVersion()).toEqual('*');
expect(testDep.isEnabled()).toBeTruthy();
var disabledDep = newConfig.getPluginDependency('disabled');
expect(disabledDep).toBeDefined();
expect(disabledDep.getVersion()).toEqual('*');
expect(disabledDep.isEnabled()).toBeFalsy();
});
});
@@ -0,0 +1,33 @@
var removePlugin = require('../removePlugin');
var Config = require('../../../models/config');
describe('removePlugin', function() {
var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled']
});
it('should remove the plugin from the list', function() {
var newConfig = removePlugin(config, 'hello');
var testDep = newConfig.getPluginDependency('hello');
expect(testDep).toNotBeDefined();
});
it('should remove the disabled plugin from the list', function() {
var newConfig = removePlugin(config, 'disabled');
var testDep = newConfig.getPluginDependency('disabled');
expect(testDep).toNotBeDefined();
});
it('should disable default plugin', function() {
var newConfig = removePlugin(config, 'search');
var disabledDep = newConfig.getPluginDependency('search');
expect(disabledDep).toBeDefined();
expect(disabledDep.getVersion()).toEqual('*');
expect(disabledDep.isEnabled()).toBeFalsy();
});
});
@@ -0,0 +1,28 @@
var togglePlugin = require('../togglePlugin');
var Config = require('../../../models/config');
describe('togglePlugin', function() {
var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled']
});
it('should enable plugin', function() {
var newConfig = togglePlugin(config, 'disabled');
var testDep = newConfig.getPluginDependency('disabled');
expect(testDep).toBeDefined();
expect(testDep.getVersion()).toEqual('*');
expect(testDep.isEnabled()).toBeTruthy();
});
it('should disable plugin', function() {
var newConfig = togglePlugin(config, 'world');
var testDep = newConfig.getPluginDependency('world');
expect(testDep).toBeDefined();
expect(testDep.getVersion()).toEqual('*');
expect(testDep.isEnabled()).toBeFalsy();
});
});
+16 -16
View File
@@ -1,25 +1,25 @@
var PluginDependency = require('../../models/pluginDependency');
var togglePlugin = require('./togglePlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/**
Add a plugin to a book's configuration
* Add a plugin to a book's configuration
* @param {Config} config
* @param {String} pluginName
* @param {String} version (optional)
* @return {Config}
*/
function addPlugin(config, pluginName, version) {
// For default plugin, we only ensure it is enabled
if (isDefaultPlugin(pluginName, version)) {
return togglePlugin(config, pluginName, true);
}
@param {Book} book
@param {String} plugin
@param {String} version (optional)
@return {Book}
*/
function addPlugin(book, plugin, version) {
var config = book.getConfig();
var deps = config.getPluginDependencies();
var dep = PluginDependency({
name: plugin,
version: version
});
var dep = PluginDependency.create(pluginName, version);
deps = deps.push(dep);
config = config.setPluginDependencies(deps);
return book.setConfig(config);
return config.setPluginDependencies(deps);
}
module.exports = addPlugin;
+13
View File
@@ -0,0 +1,13 @@
/**
* Edit configuration of a plugin
* @param {Config} config
* @param {String} plugin
* @param {Object} pluginConfig
* @return {Config}
*/
function editPlugin(config, pluginName, pluginConfig) {
return config.setValue('pluginsConfig.'+pluginName, pluginConfig);
}
module.exports = editPlugin;
+20
View File
@@ -0,0 +1,20 @@
/**
* Return the configuration for a plugin
* @param {Config} config
* @param {String} pluginName
* @return {Object}
*/
function getPluginConfig(config, pluginName) {
var pluginsConfig = config.getValues().get('pluginsConfig');
if (pluginsConfig === undefined) {
return {};
}
var pluginConf = pluginsConfig.get(pluginName);
if (pluginConf === undefined) {
return {};
} else {
return pluginConf.toJS();
}
}
module.exports = getPluginConfig;
+15
View File
@@ -0,0 +1,15 @@
/**
* Test if a plugin is listed
* @param { {List<PluginDependency}} deps
* @param {String} plugin
* @param {String} version
* @return {Boolean}
*/
function hasPlugin(deps, pluginName, version) {
return !!deps.find(function(dep) {
return dep.getName() === pluginName && (!version || dep.getVersion() === version);
});
}
module.exports = hasPlugin;
+7 -2
View File
@@ -1,5 +1,10 @@
module.exports = {
addPlugin: require('./addPlugin'),
removePlugin: require('./removePlugin')
addPlugin: require('./addPlugin'),
removePlugin: require('./removePlugin'),
togglePlugin: require('./togglePlugin'),
editPlugin: require('./editPlugin'),
hasPlugin: require('./hasPlugin'),
getPluginConfig: require('./getPluginConfig'),
isDefaultPlugin: require('./isDefaultPlugin')
};
+14
View File
@@ -0,0 +1,14 @@
var DEFAULT_PLUGINS = require('../../constants/defaultPlugins');
var hasPlugin = require('./hasPlugin');
/**
* Test if a plugin is a default one
* @param {String} plugin
* @param {String} version
* @return {Boolean}
*/
function isDefaultPlugin(pluginName, version) {
return hasPlugin(DEFAULT_PLUGINS, pluginName, version);
}
module.exports = isDefaultPlugin;
+15 -12
View File
@@ -1,22 +1,25 @@
var togglePlugin = require('./togglePlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/**
Remove a plugin from a book's configuration
@param {Book} book
@param {String} plugin
@return {Book}
*/
function removePlugin(book, pluginName) {
var config = book.getConfig();
* Remove a plugin from a book's configuration
* @param {Config} config
* @param {String} plugin
* @return {Config}
*/
function removePlugin(config, pluginName) {
var deps = config.getPluginDependencies();
// For default plugin, we have to disable it instead of removing from the list
if (isDefaultPlugin(pluginName)) {
return togglePlugin(config, pluginName, false);
}
deps = deps.filter(function(dep) {
// Remove the dependency from the list
deps = deps.filterNot(function(dep) {
return dep.getName() === pluginName;
});
config = config.setPluginDependencies(deps);
return book.setConfig(config);
return config.setPluginDependencies(deps);
}
module.exports = removePlugin;
+31
View File
@@ -0,0 +1,31 @@
var PluginDependency = require('../../models/pluginDependency');
var hasPlugin = require('./hasPlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/**
* Enable/disable a plugin dependency
* @param {Config} config
* @param {String} pluginName
* @param {Boolean} state (optional)
* @return {Config}
*/
function togglePlugin(config, pluginName, state) {
var deps = config.getPluginDependencies();
// For default plugin, we should ensure it's listed first
if (isDefaultPlugin(pluginName) && !hasPlugin(deps, pluginName)) {
deps = deps.push(PluginDependency.create(pluginName));
}
deps = deps.map(function(dep) {
if (dep.getName() === pluginName) {
return dep.toggle(state);
}
return dep;
});
return config.setPluginDependencies(deps);
}
module.exports = togglePlugin;
@@ -0,0 +1,78 @@
var Summary = require('../../../models/summary');
var SummaryArticle = require('../../../models/summaryArticle');
var File = require('../../../models/file');
describe('insertArticle', function() {
var insertArticle = require('../insertArticle');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: [
{
title: '2.1.1',
path: '2.1.1'
},
{
title: '2.1.2',
path: '2.1.2'
}
]
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('should insert an article at a given level', function() {
var article = SummaryArticle.create({
title: 'Inserted'
}, 'fake.level');
var newSummary = insertArticle(summary, article, '2.1.1');
var inserted = newSummary.getByLevel('2.1.1');
var nextOne = newSummary.getByLevel('2.1.2');
expect(inserted.getTitle()).toBe('Inserted');
expect(inserted.getLevel()).toBe('2.1.1');
expect(nextOne.getTitle()).toBe('2.1.1');
expect(nextOne.getLevel()).toBe('2.1.2');
});
it('should insert an article in last position', function() {
var article = SummaryArticle.create({
title: 'Inserted'
}, 'fake.level');
var newSummary = insertArticle(summary, article, '2.2');
var inserted = newSummary.getByLevel('2.2');
var previousOne = newSummary.getByLevel('2.1');
expect(inserted.getTitle()).toBe('Inserted');
expect(inserted.getLevel()).toBe('2.2');
expect(previousOne.getTitle()).toBe('2.1'); // Unchanged
expect(previousOne.getLevel()).toBe('2.1');
});
});
@@ -0,0 +1,60 @@
var Summary = require('../../../models/summary');
var SummaryPart = require('../../../models/summaryPart');
var File = require('../../../models/file');
describe('insertPart', function() {
var insertPart = require('../insertPart');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: []
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('should insert an part at a given level', function() {
var part = SummaryPart.create({
title: 'Inserted'
}, 'meaningless.level');
var newSummary = insertPart(summary, part, 1);
var inserted = newSummary.getPart(1);
expect(inserted.getTitle()).toBe('Inserted');
expect(newSummary.getParts().count()).toBe(3);
var otherArticle = newSummary.getByLevel('3.1');
expect(otherArticle.getTitle()).toBe('2.1');
expect(otherArticle.getLevel()).toBe('3.1');
});
it('should insert an part in last position', function() {
var part = SummaryPart.create({
title: 'Inserted'
}, 'meaningless.level');
var newSummary = insertPart(summary, part, 2);
var inserted = newSummary.getPart(2);
expect(inserted.getTitle()).toBe('Inserted');
expect(newSummary.getParts().count()).toBe(3);
});
});
@@ -0,0 +1,45 @@
var Immutable = require('immutable');
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('mergeAtLevel', function() {
var mergeAtLevel = require('../mergeAtLevel');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: []
}
]);
it('should edit a part', function() {
var beforeChildren = summary.getByLevel('1').getArticles();
var newSummary = mergeAtLevel(summary, '1', {title: 'Part O'});
var edited = newSummary.getByLevel('1');
expect(edited.getTitle()).toBe('Part O');
// Same children
expect(Immutable.is(beforeChildren, edited.getArticles())).toBe(true);
});
it('should edit a part', function() {
var beforePath = summary.getByLevel('1.2').getPath();
var newSummary = mergeAtLevel(summary, '1.2', {title: 'Renamed article'});
var edited = newSummary.getByLevel('1.2');
expect(edited.getTitle()).toBe('Renamed article');
// Same children
expect(Immutable.is(beforePath, edited.getPath())).toBe(true);
});
});
@@ -0,0 +1,68 @@
var Immutable = require('immutable');
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('moveArticle', function() {
var moveArticle = require('../moveArticle');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: [
{
title: '2.1.1',
path: '2.1.1'
},
{
title: '2.1.2',
path: '2.1.2'
}
]
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('should move an article to the same place', function() {
var newSummary = moveArticle(summary, '2.1', '2.1');
expect(Immutable.is(summary, newSummary)).toBe(true);
});
it('should move an article to an previous level', function() {
var newSummary = moveArticle(summary, '2.2', '2.1');
var moved = newSummary.getByLevel('2.1');
var other = newSummary.getByLevel('2.2');
expect(moved.getTitle()).toBe('2.2');
expect(other.getTitle()).toBe('2.1');
});
it('should move an article to a next level', function() {
var newSummary = moveArticle(summary, '2.1', '2.2');
var moved = newSummary.getByLevel('2.1');
var other = newSummary.getByLevel('2.2');
expect(moved.getTitle()).toBe('2.2');
expect(other.getTitle()).toBe('2.1');
});
});
@@ -0,0 +1,82 @@
var Immutable = require('immutable');
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('moveArticleAfter', function() {
var moveArticleAfter = require('../moveArticleAfter');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: [
{
title: '2.1.1',
path: '2.1.1'
},
{
title: '2.1.2',
path: '2.1.2'
}
]
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('moving right after itself should be invariant', function() {
var newSummary = moveArticleAfter(summary, '2.1', '2.1');
expect(Immutable.is(summary, newSummary)).toBe(true);
});
it('moving after previous one should be invariant too', function() {
var newSummary = moveArticleAfter(summary, '2.1', '2.0');
expect(Immutable.is(summary, newSummary)).toBe(true);
});
it('should move an article after a previous level', function() {
var newSummary = moveArticleAfter(summary, '2.2', '2.0');
var moved = newSummary.getByLevel('2.1');
expect(moved.getTitle()).toBe('2.2');
expect(newSummary.getByLevel('2.2').getTitle()).toBe('2.1');
});
it('should move an article after a previous and less deep level', function() {
var newSummary = moveArticleAfter(summary, '2.1.1', '2.0');
var moved = newSummary.getByLevel('2.1');
expect(moved.getTitle()).toBe('2.1.1');
expect(newSummary.getByLevel('2.2.1').getTitle()).toBe('2.1.2');
expect(newSummary.getByLevel('2.2').getTitle()).toBe('2.1');
});
it('should move an article after a next level', function() {
var newSummary = moveArticleAfter(summary, '2.1', '2.2');
var moved = newSummary.getByLevel('2.2');
expect(moved.getTitle()).toBe('2.1');
expect(newSummary.getByLevel('2.1').getTitle()).toBe('2.2');
});
});
@@ -0,0 +1,53 @@
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('removeArticle', function() {
var removeArticle = require('../removeArticle');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: [
{
title: '2.1.1',
path: '2.1.1'
},
{
title: '2.1.2',
path: '2.1.2'
}
]
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('should remove an article at a given level', function() {
var newSummary = removeArticle(summary, '2.1.1');
var removed = newSummary.getByLevel('2.1.1');
var nextOne = newSummary.getByLevel('2.1.2');
expect(removed.getTitle()).toBe('2.1.2');
expect(nextOne).toBe(null);
});
});
+17
View File
@@ -0,0 +1,17 @@
var mergeAtLevel = require('./mergeAtLevel');
/**
Edit the ref of an article
@param {Summary} summary
@param {String} level
@param {String} newRef
@return {Summary}
*/
function editArticleRef(summary, level, newRef) {
return mergeAtLevel(summary, level, {
ref: newRef
});
}
module.exports = editArticleRef;
+2 -2
View File
@@ -1,4 +1,4 @@
var editArticle = require('./editArticle');
var mergeAtLevel = require('./mergeAtLevel');
/**
Edit title of an article
@@ -9,7 +9,7 @@ var editArticle = require('./editArticle');
@return {Summary}
*/
function editArticleTitle(summary, level, newTitle) {
return editArticle(summary, level, {
return mergeAtLevel(summary, level, {
title: newTitle
});
}
-1
View File
@@ -1,4 +1,3 @@
/**
Edit title of a part in the summary
+8 -3
View File
@@ -1,8 +1,13 @@
module.exports = {
insertArticle: require('./insertArticle'),
moveArticle: require('./moveArticle'),
moveArticleAfter: require('./moveArticleAfter'),
removeArticle: require('./removeArticle'),
unshiftArticle: require('./unshiftArticle'),
editArticleTitle: require('./editArticleTitle'),
editArticleRef: require('./editArticleRef'),
editPartTitle: require('./editPartTitle'),
editArticleTitle: require('./editArticleTitle')
insertPart: require('./insertPart'),
removePart: require('./removePart'),
editPartTitle: require('./editPartTitle')
};
+22 -36
View File
@@ -1,63 +1,49 @@
var is = require('is');
var SummaryArticle = require('../../models/summaryArticle');
var editArticle = require('./editArticle');
var mergeAtLevel = require('./mergeAtLevel');
var indexArticleLevels = require('./indexArticleLevels');
/**
Get level of parent of an article
@param {String} level
@return {String}
*/
function getParentLevel(level) {
var parts = level.split('.');
return parts.slice(0, -1).join('.');
}
/**
Insert an article in a summary at a specific position
Returns a new Summary with the article at the given level, with
subsequent article shifted.
@param {Summary} summary
@param {String|Article} level: level to insert after
@param {Article} article
@param {String|Article} level: level to insert at
@return {Summary}
*/
function insertArticle(summary, level, article) {
function insertArticle(summary, article, level) {
article = SummaryArticle(article);
level = is.string(level)? level : level.getLevel();
var parentLevel = getParentLevel(level);
if (!parentLevel) {
// todo: insert new part
return summary;
}
// Get parent of the position
var parentArticle = summary.getByLevel(parentLevel);
if (!parentLevel) {
var parent = summary.getParent(level);
if (!parent) {
return summary;
}
// Find the index to insert at
var articles = parentArticle.getArticles();
var index = articles.findIndex(function(art) {
return art.getLevel() === level;
});
if (!index) {
return summary;
}
var articles = parent.getArticles();
var index = getLeafIndex(level);
// Insert the article at the right index
articles = articles.insert(index, article);
// Reindex the level from here
parentArticle = parentArticle.set('articles', articles);
parentArticle = indexArticleLevels(parentArticle);
parent = parent.set('articles', articles);
parent = indexArticleLevels(parent);
return editArticle(summary, parentLevel, parentArticle);
return mergeAtLevel(summary, parent.getLevel(), parent);
}
/**
@param {String}
@return {Number} The index of this level within its parent's children
*/
function getLeafIndex(level) {
var arr = level.split('.').map(function (char) {
return parseInt(char, 10);
});
return arr[arr.length - 1] - 1;
}
module.exports = insertArticle;
+19
View File
@@ -0,0 +1,19 @@
var SummaryPart = require('../../models/summaryPart');
var indexLevels = require('./indexLevels');
/**
Returns a new Summary with a part inserted at given index
@param {Summary} summary
@param {Part} part
@param {Number} index
@return {Summary}
*/
function insertPart(summary, part, index) {
part = SummaryPart(part);
var parts = summary.getParts().insert(index, part);
return indexLevels(summary.set('parts', parts));
}
module.exports = insertPart;
@@ -11,16 +11,17 @@ function editArticleInList(articles, level, newArticle) {
return articles.map(function(article) {
var articleLevel = article.getLevel();
if (articleLevel == level) {
if (articleLevel === level) {
// it is the article to edit
return article.merge(newArticle);
}
if (level.indexOf(articleLevel) === 0) {
} else if (level.indexOf(articleLevel) === 0) {
// it is a parent
var articles = editArticleInList(article.getArticles(), level, newArticle);
return article.set('articles', articles);
} else {
// This is not the article you are looking for
return article;
}
return article;
});
}
@@ -35,36 +36,40 @@ function editArticleInList(articles, level, newArticle) {
*/
function editArticleInPart(part, level, newArticle) {
var articles = part.getArticles();
articles = editArticleInList(articles);
articles = editArticleInList(articles, level, newArticle);
return part.set('articles', articles);
}
/**
Edit an article in a summary
Edit an article, or a part, in a summary. Does a shallow merge.
@param {Summary} summary
@param {String} level
@param {Article} newArticle
@param {Article|Part} newValue
@return {Summary}
*/
function editArticle(summary, level, newArticle) {
var parts = summary.getParts();
function mergeAtLevel(summary, level, newValue) {
var levelParts = level.split('.');
var partIndex = Number(levelParts[0]);
var partIndex = Number(levelParts[0]) -1;
var parts = summary.getParts();
var part = parts.get(partIndex);
if (!part) {
return summary;
}
part = editArticleInPart(part, level, newArticle);
parts = parts.set(partIndex, part);
var isEditingPart = levelParts.length < 2;
if (isEditingPart) {
part = part.merge(newValue);
} else {
part = editArticleInPart(part, level, newValue);
}
parts = parts.set(partIndex, part);
return summary.set('parts', parts);
}
module.exports = editArticle;
module.exports = mergeAtLevel;
+25
View File
@@ -0,0 +1,25 @@
var is = require('is');
var removeArticle = require('./removeArticle');
var insertArticle = require('./insertArticle');
/**
Returns a new summary, with the given article removed from its
origin level, and placed at the given target level.
@param {Summary} summary
@param {String|SummaryArticle} origin: level to remove
@param {String|SummaryArticle} target: the level where the article will be found
@return {Summary}
*/
function moveArticle(summary, origin, target) {
// Coerce to level
var originLevel = is.string(origin)? origin : origin.getLevel();
var targetLevel = is.string(target)? target : target.getLevel();
var article = summary.getByLevel(originLevel);
// Remove first
var removed = removeArticle(summary, originLevel);
return insertArticle(removed, article, targetLevel);
}
module.exports = moveArticle;
+60
View File
@@ -0,0 +1,60 @@
var is = require('is');
var removeArticle = require('./removeArticle');
var insertArticle = require('./insertArticle');
/**
Returns a new summary, with the an article moved after another
article. Unlike `moveArticle`, does not ensure that the article
will be found at the target's level plus one.
@param {Summary} summary
@param {String|SummaryArticle} origin
@param {String|SummaryArticle} afterTarget
@return {Summary}
*/
function moveArticleAfter(summary, origin, afterTarget) {
// Coerce to level
var originLevel = is.string(origin)? origin : origin.getLevel();
var afterTargetLevel = is.string(afterTarget)? afterTarget : afterTarget.getLevel();
var article = summary.getByLevel(originLevel);
var targetLevel = increment(afterTargetLevel);
if (targetLevel < origin) {
// Remove first
var removed = removeArticle(summary, originLevel);
// Insert then
return insertArticle(removed, article, targetLevel);
} else {
// Insert right after first
var inserted = insertArticle(summary, article, targetLevel);
// Remove old one
return removeArticle(inserted, originLevel);
}
}
/**
@param {String}
@return {Array<Number>}
*/
function levelToArray(l) {
return l.split('.').map(function (char) {
return parseInt(char, 10);
});
}
/**
@param {Array<Number>}
@return {String}
*/
function arrayToLevel(a) {
return a.join('.');
}
function increment(level) {
level = levelToArray(level);
level[level.length - 1]++;
return arrayToLevel(level);
}
module.exports = moveArticleAfter;
+37
View File
@@ -0,0 +1,37 @@
var is = require('is');
var mergeAtLevel = require('./mergeAtLevel');
var indexArticleLevels = require('./indexArticleLevels');
/**
Remove an article from a level.
@param {Summary} summary
@param {String|SummaryArticle} level: level to remove
@return {Summary}
*/
function removeArticle(summary, level) {
// Coerce to level
level = is.string(level)? level : level.getLevel();
var parent = summary.getParent(level);
var articles = parent.getArticles();
// Find the index to remove
var index = articles.findIndex(function(art) {
return art.getLevel() === level;
});
if (index === -1) {
return summary;
}
// Remove from children
articles = articles.remove(index);
parent = parent.set('articles', articles);
// Reindex the level from here
parent = indexArticleLevels(parent);
return mergeAtLevel(summary, parent.getLevel(), parent);
}
module.exports = removeArticle;
+15
View File
@@ -0,0 +1,15 @@
var indexLevels = require('./indexLevels');
/**
Remove a part at given index
@param {Summary} summary
@param {Number|} index
@return {Summary}
*/
function removePart(summary, index) {
var parts = summary.getParts().remove(index);
return indexLevels(summary.set('parts', parts));
}
module.exports = removePart;
+1 -1
View File
@@ -4,7 +4,7 @@ var SummaryPart = require('../../models/summaryPart');
var indexLevels = require('./indexLevels');
/**
Insert an article at the
Insert an article at the beginning of summary
@param {Summary} summary
@param {Article} article
+38
View File
@@ -0,0 +1,38 @@
var Immutable = require('immutable');
var Output = require('../../models/output');
var Book = require('../../models/book');
var parseBook = require('../../parse/parseBook');
var createMockFS = require('../../fs/mock');
var preparePlugins = require('../preparePlugins');
/**
* Create an output using a generator
*
* FOR TESTING PURPOSE ONLY
*
* @param {Generator} generator
* @param {Map<String:String|Map>} files
* @return {Promise<Output>}
*/
function createMockOutput(generator, files, options) {
var fs = createMockFS(files);
var book = Book.createForFS(fs);
var state = generator.State? generator.State({}) : Immutable.Map();
book = book.setLogLevel('disabled');
options = generator.Options(options);
return parseBook(book)
.then(function(resultBook) {
return new Output({
book: resultBook,
options: options,
state: state,
generator: generator.name
});
})
.then(preparePlugins);
}
module.exports = createMockOutput;
+2 -2
View File
@@ -1,9 +1,9 @@
var generateMock = require('../generateMock');
var generateMock = require('./generateMock');
var EbookGenerator = require('../ebook');
describe('EbookGenerator', function() {
pit('should generate a SUMMARY.html', function() {
it('should generate a SUMMARY.html', function() {
return generateMock(EbookGenerator, {
'README.md': 'Hello World'
})
+40
View File
@@ -0,0 +1,40 @@
var tmp = require('tmp');
var Book = require('../../models/book');
var createMockFS = require('../../fs/mock');
var parseBook = require('../../parse/parseBook');
var generateBook = require('../generateBook');
/**
* Generate a book using a generator
* And returns the path to the output dir.
*
* FOR TESTING PURPOSE ONLY
*
* @param {Generator}
* @param {Map<String:String|Map>} files
* @return {Promise<String>}
*/
function generateMock(Generator, files) {
var fs = createMockFS(files);
var book = Book.createForFS(fs);
var dir;
try {
dir = tmp.dirSync();
} catch(err) {
throw err;
}
book = book.setLogLevel('disabled');
return parseBook(book)
.then(function(resultBook) {
return generateBook(Generator, resultBook, {
root: dir.name
});
})
.thenResolve(dir.name);
}
module.exports = generateMock;
+20 -3
View File
@@ -1,9 +1,9 @@
var generateMock = require('../generateMock');
var generateMock = require('./generateMock');
var JSONGenerator = require('../json');
describe('JSONGenerator', function() {
pit('should generate a README.json', function() {
it('should generate a README.json', function() {
return generateMock(JSONGenerator, {
'README.md': 'Hello World'
})
@@ -12,7 +12,7 @@ describe('JSONGenerator', function() {
});
});
pit('should generate a json file for each articles', function() {
it('should generate a json file for each articles', function() {
return generateMock(JSONGenerator, {
'README.md': 'Hello World',
'SUMMARY.md': '# Summary\n\n* [Page](test/page.md)',
@@ -25,5 +25,22 @@ describe('JSONGenerator', function() {
expect(folder).toHaveFile('test/page.json');
});
});
it('should generate a multilingual book', function() {
return generateMock(JSONGenerator, {
'LANGS.md': '# Languages\n\n* [en](en)\n* [fr](fr)',
'en': {
'README.md': 'Hello'
},
'fr': {
'README.md': 'Bonjour'
}
})
.then(function(folder) {
expect(folder).toHaveFile('en/README.json');
expect(folder).toHaveFile('fr/README.json');
expect(folder).toHaveFile('README.json');
});
});
});

Some files were not shown because too many files have changed in this diff Show More