summaryrefslogtreecommitdiffstats
path: root/lib/parse
diff options
context:
space:
mode:
Diffstat (limited to 'lib/parse')
-rw-r--r--lib/parse/__tests__/parseBook.js55
-rw-r--r--lib/parse/__tests__/parseGlossary.js36
-rw-r--r--lib/parse/__tests__/parseIgnore.js40
-rw-r--r--lib/parse/__tests__/parseReadme.js36
-rw-r--r--lib/parse/__tests__/parseSummary.js34
-rw-r--r--lib/parse/findParsableFile.js36
-rw-r--r--lib/parse/index.js13
-rw-r--r--lib/parse/listAssets.js36
-rw-r--r--lib/parse/parseBook.js77
-rw-r--r--lib/parse/parseConfig.js51
-rw-r--r--lib/parse/parseGlossary.js26
-rw-r--r--lib/parse/parseIgnore.js50
-rw-r--r--lib/parse/parseLanguages.js28
-rw-r--r--lib/parse/parsePage.js29
-rw-r--r--lib/parse/parsePagesList.js45
-rw-r--r--lib/parse/parseReadme.js28
-rw-r--r--lib/parse/parseStructureFile.js57
-rw-r--r--lib/parse/parseSummary.js46
-rw-r--r--lib/parse/validateConfig.js31
-rw-r--r--lib/parse/walkSummary.js34
20 files changed, 788 insertions, 0 deletions
diff --git a/lib/parse/__tests__/parseBook.js b/lib/parse/__tests__/parseBook.js
new file mode 100644
index 0000000..25d1802
--- /dev/null
+++ b/lib/parse/__tests__/parseBook.js
@@ -0,0 +1,55 @@
+var Book = require('../../models/book');
+var createMockFS = require('../../fs/mock');
+
+describe('parseBook', function() {
+ var parseBook = require('../parseBook');
+
+ pit('should parse multilingual book', function() {
+ var fs = createMockFS({
+ 'LANGS.md': '# Languages\n\n* [en](en)\n* [fr](fr)',
+ 'en': {
+ 'README.md': 'Hello'
+ },
+ 'fr': {
+ 'README.md': 'Bonjour'
+ }
+ });
+ var book = Book.createForFS(fs);
+
+ return parseBook(book)
+ .then(function(resultBook) {
+ var languages = resultBook.getLanguages();
+ var books = resultBook.getBooks();
+
+ expect(resultBook.isMultilingual()).toBe(true);
+ expect(languages.getList().size).toBe(2);
+ expect(books.size).toBe(2);
+ });
+ });
+
+ pit('should parse book in a directory', function() {
+ var fs = createMockFS({
+ 'book.json': JSON.stringify({
+ root: './test'
+ }),
+ 'test': {
+ 'README.md': 'Hello World',
+ 'SUMMARY.md': '# Summary\n\n* [Page](page.md)\n',
+ 'page.md': 'Page'
+ }
+ });
+ var book = Book.createForFS(fs);
+
+ return parseBook(book)
+ .then(function(resultBook) {
+ var readme = resultBook.getReadme();
+ var summary = resultBook.getSummary();
+ var articles = summary.getArticlesAsList();
+
+ expect(summary.getFile().exists()).toBe(true);
+ expect(readme.getFile().exists()).toBe(true);
+ expect(articles.size).toBe(2);
+ });
+ });
+
+});
diff --git a/lib/parse/__tests__/parseGlossary.js b/lib/parse/__tests__/parseGlossary.js
new file mode 100644
index 0000000..53805fe
--- /dev/null
+++ b/lib/parse/__tests__/parseGlossary.js
@@ -0,0 +1,36 @@
+var Book = require('../../models/book');
+var createMockFS = require('../../fs/mock');
+
+describe('parseGlossary', function() {
+ var parseGlossary = require('../parseGlossary');
+
+ pit('should parse glossary if exists', function() {
+ var fs = createMockFS({
+ 'GLOSSARY.md': '# Glossary\n\n## Hello\nDescription for hello'
+ });
+ var book = Book.createForFS(fs);
+
+ return parseGlossary(book)
+ .then(function(resultBook) {
+ var glossary = resultBook.getGlossary();
+ var file = glossary.getFile();
+ var entries = glossary.getEntries();
+
+ expect(file.exists()).toBeTruthy();
+ expect(entries.size).toBe(1);
+ });
+ });
+
+ pit('should not fail if doesn\'t exist', function() {
+ var fs = createMockFS({});
+ var book = Book.createForFS(fs);
+
+ return parseGlossary(book)
+ .then(function(resultBook) {
+ var glossary = resultBook.getGlossary();
+ var file = glossary.getFile();
+
+ expect(file.exists()).toBeFalsy();
+ });
+ });
+});
diff --git a/lib/parse/__tests__/parseIgnore.js b/lib/parse/__tests__/parseIgnore.js
new file mode 100644
index 0000000..bee4236
--- /dev/null
+++ b/lib/parse/__tests__/parseIgnore.js
@@ -0,0 +1,40 @@
+var Book = require('../../models/book');
+var createMockFS = require('../../fs/mock');
+
+describe('parseIgnore', function() {
+ var parseIgnore = require('../parseIgnore');
+ var fs = createMockFS({
+ '.ignore': 'test-1.js',
+ '.gitignore': 'test-2.js\ntest-3.js',
+ '.bookignore': '!test-3.js',
+ 'test-1.js': '1',
+ 'test-2.js': '2',
+ 'test-3.js': '3'
+ });
+
+ function getBook() {
+ var book = Book.createForFS(fs);
+ return parseIgnore(book);
+ }
+
+ pit('should load rules from .ignore', function() {
+ return getBook()
+ .then(function(book) {
+ expect(book.isFileIgnored('test-1.js')).toBeTruthy();
+ });
+ });
+
+ pit('should load rules from .gitignore', function() {
+ return getBook()
+ .then(function(book) {
+ expect(book.isFileIgnored('test-2.js')).toBeTruthy();
+ });
+ });
+
+ pit('should load rules from .bookignore', function() {
+ return getBook()
+ .then(function(book) {
+ expect(book.isFileIgnored('test-3.js')).toBeFalsy();
+ });
+ });
+});
diff --git a/lib/parse/__tests__/parseReadme.js b/lib/parse/__tests__/parseReadme.js
new file mode 100644
index 0000000..1b1567b
--- /dev/null
+++ b/lib/parse/__tests__/parseReadme.js
@@ -0,0 +1,36 @@
+var Promise = require('../../utils/promise');
+var Book = require('../../models/book');
+var createMockFS = require('../../fs/mock');
+
+describe('parseReadme', function() {
+ var parseReadme = require('../parseReadme');
+
+ pit('should parse summary if exists', function() {
+ var fs = createMockFS({
+ 'README.md': '# Hello\n\nAnd here is the description.'
+ });
+ var book = Book.createForFS(fs);
+
+ return parseReadme(book)
+ .then(function(resultBook) {
+ var readme = resultBook.getReadme();
+ var file = readme.getFile();
+
+ expect(file.exists()).toBeTruthy();
+ expect(readme.getTitle()).toBe('Hello');
+ expect(readme.getDescription()).toBe('And here is the description.');
+ });
+ });
+
+ pit('should fail if doesn\'t exist', function() {
+ var fs = createMockFS({});
+ var book = Book.createForFS(fs);
+
+ return parseReadme(book)
+ .then(function(resultBook) {
+ throw new Error('It should have fail');
+ }, function() {
+ return Promise();
+ });
+ });
+});
diff --git a/lib/parse/__tests__/parseSummary.js b/lib/parse/__tests__/parseSummary.js
new file mode 100644
index 0000000..4b4650d
--- /dev/null
+++ b/lib/parse/__tests__/parseSummary.js
@@ -0,0 +1,34 @@
+var Book = require('../../models/book');
+var createMockFS = require('../../fs/mock');
+
+describe('parseSummary', function() {
+ var parseSummary = require('../parseSummary');
+
+ pit('should parse summary if exists', function() {
+ var fs = createMockFS({
+ 'SUMMARY.md': '# Summary\n\n* [Hello](hello.md)'
+ });
+ var book = Book.createForFS(fs);
+
+ return parseSummary(book)
+ .then(function(resultBook) {
+ var summary = resultBook.getSummary();
+ var file = summary.getFile();
+
+ expect(file.exists()).toBeTruthy();
+ });
+ });
+
+ pit('should not fail if doesn\'t exist', function() {
+ var fs = createMockFS({});
+ var book = Book.createForFS(fs);
+
+ return parseSummary(book)
+ .then(function(resultBook) {
+ var summary = resultBook.getSummary();
+ var file = summary.getFile();
+
+ expect(file.exists()).toBeFalsy();
+ });
+ });
+});
diff --git a/lib/parse/findParsableFile.js b/lib/parse/findParsableFile.js
new file mode 100644
index 0000000..4434d64
--- /dev/null
+++ b/lib/parse/findParsableFile.js
@@ -0,0 +1,36 @@
+var path = require('path');
+
+var Promise = require('../utils/promise');
+var parsers = require('../parsers');
+
+/**
+ Find a file parsable (Markdown or AsciiDoc) in a book
+
+ @param {Book} book
+ @param {String} filename
+ @return {Promise<>}
+*/
+function findParsableFile(book, filename) {
+ var fs = book.getContentFS();
+ var ext = path.extname(filename);
+ var basename = path.basename(filename, ext);
+ var basedir = path.dirname(filename);
+
+ // Ordered list of extensions to test
+ var exts = parsers.extensions;
+
+ return Promise.some(exts, function(ext) {
+ var filepath = basename + ext;
+
+ return fs.findFile(basedir, filepath)
+ .then(function(found) {
+ if (!found || book.isContentFileIgnored(found)) {
+ return undefined;
+ }
+
+ return fs.statFile(found);
+ });
+ });
+}
+
+module.exports = findParsableFile;
diff --git a/lib/parse/index.js b/lib/parse/index.js
new file mode 100644
index 0000000..ac27fcf
--- /dev/null
+++ b/lib/parse/index.js
@@ -0,0 +1,13 @@
+
+module.exports = {
+ parseBook: require('./parseBook'),
+ parseSummary: require('./parseSummary'),
+ parseGlossary: require('./parseGlossary'),
+ parseReadme: require('./parseReadme'),
+ parseConfig: require('./parseConfig'),
+ parsePagesList: require('./parsePagesList'),
+ parseIgnore: require('./parseIgnore'),
+ listAssets: require('./listAssets'),
+ parseLanguages: require('./parseLanguages'),
+ parsePage: require('./parsePage')
+};
diff --git a/lib/parse/listAssets.js b/lib/parse/listAssets.js
new file mode 100644
index 0000000..c43b054
--- /dev/null
+++ b/lib/parse/listAssets.js
@@ -0,0 +1,36 @@
+var timing = require('../utils/timing');
+
+/**
+ List all assets in a book
+ Assets are file not ignored and not a page
+
+ @param {Book} book
+ @param {List<String>} pages
+ @param
+*/
+function listAssets(book, pages) {
+ var fs = book.getContentFS();
+
+ var summary = book.getSummary();
+ var summaryFile = summary.getFile().getPath();
+
+ var glossary = book.getGlossary();
+ var glossaryFile = glossary.getFile().getPath();
+
+ return timing.measure(
+ 'parse.listAssets',
+ fs.listAllFiles()
+ .then(function(files) {
+ return files.filterNot(function(file) {
+ return (
+ book.isContentFileIgnored(file) ||
+ pages.has(file) ||
+ file !== summaryFile ||
+ file !== glossaryFile
+ );
+ });
+ })
+ );
+}
+
+module.exports = listAssets;
diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js
new file mode 100644
index 0000000..84a4038
--- /dev/null
+++ b/lib/parse/parseBook.js
@@ -0,0 +1,77 @@
+var Promise = require('../utils/promise');
+var timing = require('../utils/timing');
+var Book = require('../models/book');
+
+var parseIgnore = require('./parseIgnore');
+var parseConfig = require('./parseConfig');
+var parseGlossary = require('./parseGlossary');
+var parseSummary = require('./parseSummary');
+var parseReadme = require('./parseReadme');
+var parseLanguages = require('./parseLanguages');
+
+/**
+ Parse content of a book
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseBookContent(book) {
+ return Promise(book)
+ .then(parseReadme)
+ .then(parseSummary)
+ .then(parseGlossary);
+}
+
+/**
+ Parse a multilingual book
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseMultilingualBook(book) {
+ var languages = book.getLanguages();
+ var langList = languages.getList();
+
+ return Promise.reduce(langList, function(currentBook, lang) {
+ var langID = lang.getID();
+ var child = Book.createFromParent(currentBook, langID);
+ var ignore = currentBook.getIgnore();
+
+ return Promise(child)
+ .then(parseConfig)
+ .then(parseBookContent)
+ .then(function(result) {
+ // Ignore content of this book when generating parent book
+ ignore.add(langID + '/**');
+ currentBook = currentBook.set('ignore', ignore);
+
+ return currentBook.addLanguageBook(langID, result);
+ });
+ }, book);
+}
+
+
+/**
+ Parse a whole book from a filesystem
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseBook(book) {
+ return timing.measure(
+ 'parse.book',
+ Promise(book)
+ .then(parseIgnore)
+ .then(parseConfig)
+ .then(parseLanguages)
+ .then(function(resultBook) {
+ if (resultBook.isMultilingual()) {
+ return parseMultilingualBook(resultBook);
+ } else {
+ return parseBookContent(resultBook);
+ }
+ })
+ );
+}
+
+module.exports = parseBook;
diff --git a/lib/parse/parseConfig.js b/lib/parse/parseConfig.js
new file mode 100644
index 0000000..5200de2
--- /dev/null
+++ b/lib/parse/parseConfig.js
@@ -0,0 +1,51 @@
+var Promise = require('../utils/promise');
+var Config = require('../models/config');
+
+var File = require('../models/file');
+var validateConfig = require('./validateConfig');
+var CONFIG_FILES = require('../constants/configFiles');
+
+/**
+ Parse configuration from "book.json" or "book.js"
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseConfig(book) {
+ var fs = book.getFS();
+
+ return Promise.some(CONFIG_FILES, function(filename) {
+ // Is this file ignored?
+ if (book.isFileIgnored(filename)) {
+ return;
+ }
+
+ // Try loading it
+ return Promise.all([
+ fs.loadAsObject(filename),
+ fs.statFile(filename)
+ ])
+ .spread(function(cfg, file) {
+ return {
+ file: file,
+ values: cfg
+ };
+ })
+ .fail(function(err) {
+ if (err.code != 'MODULE_NOT_FOUND') throw(err);
+ else return Promise(false);
+ });
+ })
+
+ .then(function(result) {
+ var file = result? result.file : File();
+ var values = result? result.values : {};
+
+ values = validateConfig(values);
+
+ var config = Config.create(file, values);
+ return book.set('config', config);
+ });
+}
+
+module.exports = parseConfig;
diff --git a/lib/parse/parseGlossary.js b/lib/parse/parseGlossary.js
new file mode 100644
index 0000000..a96e5fc
--- /dev/null
+++ b/lib/parse/parseGlossary.js
@@ -0,0 +1,26 @@
+var parseStructureFile = require('./parseStructureFile');
+var Glossary = require('../models/glossary');
+
+/**
+ Parse glossary
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseGlossary(book) {
+ var logger = book.getLogger();
+
+ return parseStructureFile(book, 'glossary')
+ .spread(function(file, entries) {
+ if (!file) {
+ return book;
+ }
+
+ logger.debug.ln('glossary index file found at', file.getPath());
+
+ var glossary = Glossary.createFromEntries(file, entries);
+ return book.set('glossary', glossary);
+ });
+}
+
+module.exports = parseGlossary;
diff --git a/lib/parse/parseIgnore.js b/lib/parse/parseIgnore.js
new file mode 100644
index 0000000..b23bfd8
--- /dev/null
+++ b/lib/parse/parseIgnore.js
@@ -0,0 +1,50 @@
+var Promise = require('../utils/promise');
+var IGNORE_FILES = require('../constants/ignoreFiles');
+
+/**
+ Parse ignore files
+
+ @param {Book}
+ @return {Book}
+*/
+function parseIgnore(book) {
+ if (book.isLanguageBook()) {
+ return Promise.reject(new Error('Ignore files could be parsed for language books'));
+ }
+
+ var fs = book.getFS();
+ var ignore = book.getIgnore();
+
+ ignore.addPattern([
+ // Skip Git stuff
+ '.git/',
+
+ // Skip OS X meta data
+ '.DS_Store',
+
+ // Skip stuff installed by plugins
+ 'node_modules',
+
+ // Skip book outputs
+ '_book',
+ '*.pdf',
+ '*.epub',
+ '*.mobi',
+
+ // Ignore files in the templates folder
+ '_layouts'
+ ]);
+
+ return Promise.serie(IGNORE_FILES, function(filename) {
+ return fs.readAsString(filename)
+ .then(function(content) {
+ ignore.addPattern(content.toString().split(/\r?\n/));
+ }, function(err) {
+ return Promise();
+ });
+ })
+
+ .thenResolve(book);
+}
+
+module.exports = parseIgnore;
diff --git a/lib/parse/parseLanguages.js b/lib/parse/parseLanguages.js
new file mode 100644
index 0000000..346f3a3
--- /dev/null
+++ b/lib/parse/parseLanguages.js
@@ -0,0 +1,28 @@
+var parseStructureFile = require('./parseStructureFile');
+var Languages = require('../models/languages');
+
+/**
+ Parse languages list from book
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseLanguages(book) {
+ var logger = book.getLogger();
+
+ return parseStructureFile(book, 'langs')
+ .spread(function(file, result) {
+ if (!file) {
+ return book;
+ }
+
+ var languages = Languages.createFromList(file, result);
+
+ logger.debug.ln('languages index file found at', file.getPath());
+ logger.info.ln('parsing multilingual book, with', languages.getList().size, 'languages');
+
+ return book.set('languages', languages);
+ });
+}
+
+module.exports = parseLanguages;
diff --git a/lib/parse/parsePage.js b/lib/parse/parsePage.js
new file mode 100644
index 0000000..1d515d6
--- /dev/null
+++ b/lib/parse/parsePage.js
@@ -0,0 +1,29 @@
+var Immutable = require('immutable');
+var fm = require('front-matter');
+var direction = require('direction');
+
+/**
+ Parse a page, read its content and parse the YAMl header
+
+ @param {Book} book
+ @param {Page} page
+ @return {Promise<Page>}
+*/
+function parsePage(book, page) {
+ var fs = book.getContentFS();
+ var file = page.getFile();
+
+ return fs.readAsString(file.getPath())
+ .then(function(content) {
+ var parsed = fm(content);
+
+ return page.merge({
+ content: parsed.body,
+ attributes: Immutable.fromJS(parsed.attributes),
+ dir: direction(parsed.body)
+ });
+ });
+}
+
+
+module.exports = parsePage;
diff --git a/lib/parse/parsePagesList.js b/lib/parse/parsePagesList.js
new file mode 100644
index 0000000..a3a52f8
--- /dev/null
+++ b/lib/parse/parsePagesList.js
@@ -0,0 +1,45 @@
+var Immutable = require('immutable');
+
+var timing = require('../utils/timing');
+var Page = require('../models/page');
+var walkSummary = require('./walkSummary');
+
+/**
+ Parse all pages from a book as an OrderedMap
+
+ @param {Book} book
+ @return {Promise<OrderedMap<Page>>}
+*/
+function parsePagesList(book) {
+ var fs = book.getContentFS();
+ var summary = book.getSummary();
+ var map = Immutable.OrderedMap();
+
+ return timing.measure(
+ 'parse.listPages',
+ walkSummary(summary, function(article) {
+ if (!article.isPage()) return;
+
+ var filepath = article.getPath();
+
+ // Is the page ignored?
+ if (book.isContentFileIgnored(filepath)) return;
+
+ return fs.statFile(filepath)
+ .then(function(file) {
+ map = map.set(
+ filepath,
+ Page.createForFile(file)
+ );
+ }, function() {
+ // file doesn't exist
+ });
+ })
+ .then(function() {
+ return map;
+ })
+ );
+}
+
+
+module.exports = parsePagesList;
diff --git a/lib/parse/parseReadme.js b/lib/parse/parseReadme.js
new file mode 100644
index 0000000..a2ede77
--- /dev/null
+++ b/lib/parse/parseReadme.js
@@ -0,0 +1,28 @@
+var parseStructureFile = require('./parseStructureFile');
+var Readme = require('../models/readme');
+
+var error = require('../utils/error');
+
+/**
+ Parse readme from book
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseReadme(book) {
+ var logger = book.getLogger();
+
+ return parseStructureFile(book, 'readme')
+ .spread(function(file, result) {
+ if (!file) {
+ throw new error.FileNotFoundError({ filename: 'README' });
+ }
+
+ logger.debug.ln('readme found at', file.getPath());
+
+ var readme = Readme.create(file, result);
+ return book.set('readme', readme);
+ });
+}
+
+module.exports = parseReadme;
diff --git a/lib/parse/parseStructureFile.js b/lib/parse/parseStructureFile.js
new file mode 100644
index 0000000..bdb97db
--- /dev/null
+++ b/lib/parse/parseStructureFile.js
@@ -0,0 +1,57 @@
+var findParsableFile = require('./findParsableFile');
+var Promise = require('../utils/promise');
+var error = require('../utils/error');
+
+/**
+ Parse a ParsableFile using a specific method
+
+ @param {FS} fs
+ @param {ParsableFile} file
+ @param {String} type
+ @return {Promise<Array<String, List|Map>>}
+*/
+function parseFile(fs, file, type) {
+ var filepath = file.getPath();
+ var parser = file.getParser();
+
+ if (!parser) {
+ return Promise.reject(
+ error.FileNotParsableError({
+ filename: filepath
+ })
+ );
+ }
+
+ return fs.readAsString(filepath)
+ .then(function(content) {
+ return [
+ file,
+ parser[type](content)
+ ];
+ });
+}
+
+
+/**
+ Parse a structure file (ex: SUMMARY.md, GLOSSARY.md).
+ It uses the configuration to find the specified file.
+
+ @param {Book} book
+ @param {String} type: one of ["glossary", "readme", "summary"]
+ @return {Promise<List|Map>}
+*/
+function parseStructureFile(book, type) {
+ var fs = book.getContentFS();
+ var config = book.getConfig();
+
+ var fileToSearch = config.getValue(['structure', type]);
+
+ return findParsableFile(book, fileToSearch)
+ .then(function(file) {
+ if (!file) return [undefined, undefined];
+
+ return parseFile(fs, file, type);
+ });
+}
+
+module.exports = parseStructureFile;
diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js
new file mode 100644
index 0000000..72bf224
--- /dev/null
+++ b/lib/parse/parseSummary.js
@@ -0,0 +1,46 @@
+var parseStructureFile = require('./parseStructureFile');
+var Summary = require('../models/summary');
+var SummaryModifier = require('../modifiers').Summary;
+var location = require('../utils/location');
+
+/**
+ Parse summary in a book, the summary can only be parsed
+ if the readme as be detected before.
+
+ @param {Book} book
+ @return {Promise<Book>}
+*/
+function parseSummary(book) {
+ var readme = book.getReadme();
+ var logger = book.getLogger();
+ var readmeFile = readme.getFile();
+
+ return parseStructureFile(book, 'summary')
+ .spread(function(file, result) {
+ var summary;
+
+ if (!file) {
+ logger.warn.ln('no summary file in this book');
+ summary = Summary();
+ } else {
+ logger.debug.ln('summary file found at', file.getPath());
+ summary = Summary.createFromParts(file, result.parts);
+ }
+
+ // Insert readme as first entry
+ var firstArticle = summary.getFirstArticle();
+
+ if (readmeFile.exists() &&
+ (!firstArticle || !location.areIdenticalPaths(firstArticle.getRef(), readmeFile.getPath()))) {
+ summary = SummaryModifier.unshiftArticle(summary, {
+ title: 'Introduction',
+ ref: readmeFile.getPath()
+ });
+ }
+
+ // Set new summary
+ return book.setSummary(summary);
+ });
+}
+
+module.exports = parseSummary;
diff --git a/lib/parse/validateConfig.js b/lib/parse/validateConfig.js
new file mode 100644
index 0000000..855edc3
--- /dev/null
+++ b/lib/parse/validateConfig.js
@@ -0,0 +1,31 @@
+var jsonschema = require('jsonschema');
+var jsonSchemaDefaults = require('json-schema-defaults');
+var mergeDefaults = require('merge-defaults');
+
+var schema = require('../constants/configSchema');
+var error = require('../utils/error');
+
+/**
+ Validate a book.json content
+ And return a mix with the default value
+
+ @param {Object} bookJson
+ @return {Object}
+*/
+function validateConfig(bookJson) {
+ var v = new jsonschema.Validator();
+ var result = v.validate(bookJson, schema, {
+ propertyName: 'config'
+ });
+
+ // Throw error
+ if (result.errors.length > 0) {
+ throw new error.ConfigurationError(new Error(result.errors[0].stack));
+ }
+
+ // Insert default values
+ var defaults = jsonSchemaDefaults(schema);
+ return mergeDefaults(bookJson, defaults);
+}
+
+module.exports = validateConfig;
diff --git a/lib/parse/walkSummary.js b/lib/parse/walkSummary.js
new file mode 100644
index 0000000..0117752
--- /dev/null
+++ b/lib/parse/walkSummary.js
@@ -0,0 +1,34 @@
+var Promise = require('../utils/promise');
+
+/**
+ Walk over a list of articles
+
+ @param {List<Article>} articles
+ @param {Function(article)}
+ @return {Promise}
+*/
+function walkArticles(articles, fn) {
+ return Promise.forEach(articles, function(article) {
+ return Promise(fn(article))
+ .then(function() {
+ return walkArticles(article.getArticles(), fn);
+ });
+ });
+}
+
+/**
+ Walk over summary and execute "fn" on each article
+
+ @param {Summary} summary
+ @param {Function(article)}
+ @return {Promise}
+*/
+function walkSummary(summary, fn) {
+ var parts = summary.getParts();
+
+ return Promise.forEach(parts, function(part) {
+ return walkArticles(part.getArticles(), fn);
+ });
+}
+
+module.exports = walkSummary;