diff options
Diffstat (limited to 'lib/parse')
-rw-r--r-- | lib/parse/__tests__/parseBook.js | 55 | ||||
-rw-r--r-- | lib/parse/__tests__/parseGlossary.js | 36 | ||||
-rw-r--r-- | lib/parse/__tests__/parseIgnore.js | 40 | ||||
-rw-r--r-- | lib/parse/__tests__/parseReadme.js | 36 | ||||
-rw-r--r-- | lib/parse/__tests__/parseSummary.js | 34 | ||||
-rw-r--r-- | lib/parse/findParsableFile.js | 36 | ||||
-rw-r--r-- | lib/parse/index.js | 13 | ||||
-rw-r--r-- | lib/parse/listAssets.js | 36 | ||||
-rw-r--r-- | lib/parse/parseBook.js | 77 | ||||
-rw-r--r-- | lib/parse/parseConfig.js | 51 | ||||
-rw-r--r-- | lib/parse/parseGlossary.js | 26 | ||||
-rw-r--r-- | lib/parse/parseIgnore.js | 50 | ||||
-rw-r--r-- | lib/parse/parseLanguages.js | 28 | ||||
-rw-r--r-- | lib/parse/parsePage.js | 29 | ||||
-rw-r--r-- | lib/parse/parsePagesList.js | 45 | ||||
-rw-r--r-- | lib/parse/parseReadme.js | 28 | ||||
-rw-r--r-- | lib/parse/parseStructureFile.js | 57 | ||||
-rw-r--r-- | lib/parse/parseSummary.js | 46 | ||||
-rw-r--r-- | lib/parse/validateConfig.js | 31 | ||||
-rw-r--r-- | lib/parse/walkSummary.js | 34 |
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; |