diff options
author | Samy Pesse <samypesse@gmail.com> | 2016-10-06 11:21:08 +0200 |
---|---|---|
committer | Samy Pesse <samypesse@gmail.com> | 2016-10-06 11:21:08 +0200 |
commit | 4119917555f827b0bff256a8e34a1deef5f4b87e (patch) | |
tree | 2304bd9587340dad1a0d35a077fbd43c1aaaa34b | |
parent | 9d20a9afa5603bcc703c6787c2ad41d124997fab (diff) | |
download | gitbook-4119917555f827b0bff256a8e34a1deef5f4b87e.zip gitbook-4119917555f827b0bff256a8e34a1deef5f4b87e.tar.gz gitbook-4119917555f827b0bff256a8e34a1deef5f4b87e.tar.bz2 |
Add class URIIndex to store table path to url
-rw-r--r-- | packages/gitbook/src/models/__tests__/uriIndex.js | 71 | ||||
-rw-r--r-- | packages/gitbook/src/models/book.js | 598 | ||||
-rw-r--r-- | packages/gitbook/src/models/output.js | 2 | ||||
-rw-r--r-- | packages/gitbook/src/models/uriIndex.js | 114 |
4 files changed, 483 insertions, 302 deletions
diff --git a/packages/gitbook/src/models/__tests__/uriIndex.js b/packages/gitbook/src/models/__tests__/uriIndex.js new file mode 100644 index 0000000..db3b13c --- /dev/null +++ b/packages/gitbook/src/models/__tests__/uriIndex.js @@ -0,0 +1,71 @@ +const URIIndex = require('../uriIndex'); + +describe.only('URIIndex', () => { + let index; + + before(() => { + index = new URIIndex({ + 'README.md': 'index.html', + 'world.md': 'world.html', + 'hello/README.md': 'hello/index.html', + 'hello/test.md': 'hello/test.html' + }); + }); + + describe('.resolve', () => { + + it('should resolve a basic file path', () => { + expect(index.resolve('README.md')).toBe('index.html'); + }); + + it('should resolve a nested file path', () => { + expect(index.resolve('hello/test.md')).toBe('hello/test.html'); + }); + + it('should normalize path', () => { + expect(index.resolve('./hello//test.md')).toBe('hello/test.html'); + }); + + it('should not fail for non existing entries', () => { + expect(index.resolve('notfound.md')).toBe('notfound.md'); + }); + + it('should not fail for absolute url', () => { + expect(index.resolve('http://google.fr')).toBe('http://google.fr'); + }); + + it('should preserve hash', () => { + expect(index.resolve('hello/test.md#myhash')).toBe('hello/test.html#myhash'); + }); + + }); + + describe('.resolveFrom', () => { + + it('should resolve correctly in same directory', () => { + expect(index.resolveFrom('README.md', 'world.md')).toBe('world.html'); + }); + + it('should resolve correctly for a nested path', () => { + expect(index.resolveFrom('README.md', 'hello/README.md')).toBe('hello/index.html'); + }); + + it('should resolve correctly for a nested path (2)', () => { + expect(index.resolveFrom('hello/README.md', 'test.md')).toBe('test.html'); + }); + + it('should resolve correctly for a nested path (3)', () => { + expect(index.resolveFrom('hello/README.md', '../README.md')).toBe('../index.html'); + }); + + it('should preserve hash', () => { + expect(index.resolveFrom('README.md', 'hello/README.md#myhash')).toBe('hello/index.html#myhash'); + }); + + it('should not fail for absolute url', () => { + expect(index.resolveFrom('README.md', 'http://google.fr')).toBe('http://google.fr'); + }); + + }); + +}); diff --git a/packages/gitbook/src/models/book.js b/packages/gitbook/src/models/book.js index 4164536..c96843b 100644 --- a/packages/gitbook/src/models/book.js +++ b/packages/gitbook/src/models/book.js @@ -1,5 +1,5 @@ const path = require('path'); -const Immutable = require('immutable'); +const { Record, OrderedMap } = require('immutable'); const Logger = require('../utils/logger'); @@ -10,355 +10,351 @@ const Summary = require('./summary'); const Glossary = require('./glossary'); const Languages = require('./languages'); const Ignore = require('./ignore'); +const URIIndex = require('./uriIndex'); -const Book = Immutable.Record({ - // Logger for outptu message - logger: Logger(), - +const DEFAULTS = { + // Logger for output message + logger: new Logger(), // Filesystem binded to the book scope to read files/directories - fs: FS(), - + fs: new FS(), // Ignore files parser - ignore: Ignore(), - + ignore: new Ignore(), // Structure files - config: Config(), - readme: Readme(), - summary: Summary(), - glossary: Glossary(), - languages: Languages(), - + config: new Config(), + readme: new Readme(), + summary: new Summary(), + glossary: new Glossary(), + languages: new Languages(), + // Index of urls + urls: new URIIndex(), // ID of the language for language books language: String(), - // List of children, if multilingual (String -> Book) - books: Immutable.OrderedMap() -}); - -Book.prototype.getLogger = function() { - return this.get('logger'); -}; - -Book.prototype.getFS = function() { - return this.get('fs'); -}; - -Book.prototype.getIgnore = function() { - return this.get('ignore'); -}; - -Book.prototype.getConfig = function() { - return this.get('config'); -}; - -Book.prototype.getReadme = function() { - return this.get('readme'); -}; - -Book.prototype.getSummary = function() { - return this.get('summary'); -}; - -Book.prototype.getGlossary = function() { - return this.get('glossary'); -}; - -Book.prototype.getLanguages = function() { - return this.get('languages'); -}; - -Book.prototype.getBooks = function() { - return this.get('books'); + books: new OrderedMap() }; -Book.prototype.getLanguage = function() { - return this.get('language'); -}; - -/** - Return FS instance to access the content - - @return {FS} -*/ -Book.prototype.getContentFS = function() { - const fs = this.getFS(); - const config = this.getConfig(); - const rootFolder = config.getValue('root'); - - if (rootFolder) { - return FS.reduceScope(fs, rootFolder); +class Book extends Record(DEFAULTS) { + getLogger() { + return this.get('logger'); } - return fs; -}; - -/** - Return root of the book - - @return {String} -*/ -Book.prototype.getRoot = function() { - const fs = this.getFS(); - return fs.getRoot(); -}; - -/** - Return root for content of the book - - @return {String} -*/ -Book.prototype.getContentRoot = function() { - const fs = this.getContentFS(); - return fs.getRoot(); -}; - -/** - Check if a file is ignore (should not being parsed, etc) - - @param {String} ref - @return {Page|undefined} -*/ -Book.prototype.isFileIgnored = function(filename) { - const ignore = this.getIgnore(); - const language = this.getLanguage(); - - // Ignore is always relative to the root of the main book - if (language) { - filename = path.join(language, filename); + getFS() { + return this.get('fs'); } - return ignore.isFileIgnored(filename); -}; - -/** - Check if a content file is ignore (should not being parsed, etc) + getIgnore() { + return this.get('ignore'); + } - @param {String} ref - @return {Page|undefined} -*/ -Book.prototype.isContentFileIgnored = function(filename) { - const config = this.getConfig(); - const rootFolder = config.getValue('root'); + getConfig() { + return this.get('config'); + } - if (rootFolder) { - filename = path.join(rootFolder, filename); + getReadme() { + return this.get('readme'); } - return this.isFileIgnored(filename); -}; + getSummary() { + return this.get('summary'); + } -/** - Return a page from a book by its path + getGlossary() { + return this.get('glossary'); + } - @param {String} ref - @return {Page|undefined} -*/ -Book.prototype.getPage = function(ref) { - return this.getPages().get(ref); -}; + getLanguages() { + return this.get('languages'); + } -/** - Is this book the parent of language's books + getBooks() { + return this.get('books'); + } - @return {Boolean} -*/ -Book.prototype.isMultilingual = function() { - return (this.getLanguages().getCount() > 0); -}; + getLanguage() { + return this.get('language'); + } -/** - Return true if book is associated to a language + /** + * Return FS instance to access the content + * @return {FS} + */ + getContentFS() { + const fs = this.getFS(); + const config = this.getConfig(); + const rootFolder = config.getValue('root'); + + if (rootFolder) { + return FS.reduceScope(fs, rootFolder); + } - @return {Boolean} -*/ -Book.prototype.isLanguageBook = function() { - return Boolean(this.getLanguage()); -}; + return fs; + } -/** - Return a languages book + /** + * Return root of the book + * + * @return {String} + */ + getRoot() { + const fs = this.getFS(); + return fs.getRoot(); + } - @param {String} language - @return {Book} -*/ -Book.prototype.getLanguageBook = function(language) { - const books = this.getBooks(); - return books.get(language); -}; + /** + * Return root for content of the book + * + * @return {String} + */ + getContentRoot() { + const fs = this.getContentFS(); + return fs.getRoot(); + } -/** - Add a new language book + /** + * Check if a file is ignore (should not being parsed, etc) + * + * @param {String} ref + * @return {Page|undefined} + */ + isFileIgnored(filename) { + const ignore = this.getIgnore(); + const language = this.getLanguage(); + + // Ignore is always relative to the root of the main book + if (language) { + filename = path.join(language, filename); + } - @param {String} language - @param {Book} book - @return {Book} -*/ -Book.prototype.addLanguageBook = function(language, book) { - let books = this.getBooks(); - books = books.set(language, book); + return ignore.isFileIgnored(filename); + } - return this.set('books', books); -}; + /** + * Check if a content file is ignore (should not being parsed, etc) + * + * @param {String} ref + * @return {Page|undefined} + */ + isContentFileIgnored(filename) { + const config = this.getConfig(); + const rootFolder = config.getValue('root'); + + if (rootFolder) { + filename = path.join(rootFolder, filename); + } -/** - Set the summary for this book + return this.isFileIgnored(filename); + } - @param {Summary} - @return {Book} -*/ -Book.prototype.setSummary = function(summary) { - return this.set('summary', summary); -}; + /** + * Return a page from a book by its path + * + * @param {String} ref + * @return {Page|undefined} + */ + getPage(ref) { + return this.getPages().get(ref); + } -/** - Set the readme for this book + /** + * Is this book the parent of language's books + * @return {Boolean} + */ + isMultilingual() { + return (this.getLanguages().getCount() > 0); + } - @param {Readme} - @return {Book} -*/ -Book.prototype.setReadme = function(readme) { - return this.set('readme', readme); -}; + /** + * Return true if book is associated to a language + * @return {Boolean} + */ + isLanguageBook() { + return Boolean(this.getLanguage()); + } -/** - Set the configuration for this book + /** + * Return a languages book + * @param {String} language + * @return {Book} + */ + getLanguageBook(language) { + const books = this.getBooks(); + return books.get(language); + } - @param {Config} - @return {Book} -*/ -Book.prototype.setConfig = function(config) { - return this.set('config', config); -}; + /** + * Add a new language book + * + * @param {String} language + * @param {Book} book + * @return {Book} + */ + addLanguageBook(language, book) { + let books = this.getBooks(); + books = books.set(language, book); + + return this.set('books', books); + } -/** - Set the ignore instance for this book + /** + * Set the summary for this book + * + * @param {Summary} + * @return {Book} + */ + setSummary(summary) { + return this.set('summary', summary); + } - @param {Ignore} - @return {Book} -*/ -Book.prototype.setIgnore = function(ignore) { - return this.set('ignore', ignore); -}; + /** + * Set the readme for this book + * + * @param {Readme} + * @return {Book} + */ + setReadme(readme) { + return this.set('readme', readme); + } -/** - Change log level + /** + * Set the configuration for this book + * + * @param {Config} + * @return {Book} + */ + setConfig(config) { + return this.set('config', config); + } - @param {String} level - @return {Book} -*/ -Book.prototype.setLogLevel = function(level) { - this.getLogger().setLevel(level); - return this; -}; + /** + * Set the ignore instance for this book + * + @param {Ignore} + * @return {Book} + */ + setIgnore(ignore) { + return this.set('ignore', ignore); + } -/** - Create a book using a filesystem + /** + * Change log level + * + * @param {String} level + * @return {Book} + */ + setLogLevel(level) { + this.getLogger().setLevel(level); + return this; + } - @param {FS} fs - @return {Book} -*/ -Book.createForFS = function createForFS(fs) { - return new Book({ - fs - }); -}; + /** + * Infers the default extension for files + * @return {String} + */ + getDefaultExt() { + // Inferring sources + const clues = [ + this.getReadme(), + this.getSummary(), + this.getGlossary() + ]; + + // List their extensions + const exts = clues.map(function(clue) { + const file = clue.getFile(); + if (file.exists()) { + return file.getParser().getExtensions().first(); + } else { + return null; + } + }); + // Adds the general default extension + exts.push('.md'); + + // Choose the first non null + return exts.find(function(e) { return e !== null; }); + } -/** - Infers the default extension for files - @return {String} -*/ -Book.prototype.getDefaultExt = function() { - // Inferring sources - const clues = [ - this.getReadme(), - this.getSummary(), - this.getGlossary() - ]; - - // List their extensions - const exts = clues.map(function(clue) { - const file = clue.getFile(); - if (file.exists()) { - return file.getParser().getExtensions().first(); + /** + * Infer the default path for a Readme. + * @param {Boolean} [absolute=false] False for a path relative to + * this book's content root + * @return {String} + */ + getDefaultReadmePath(absolute) { + const defaultPath = 'README' + this.getDefaultExt(); + if (absolute) { + return path.join(this.getContentRoot(), defaultPath); } else { - return null; + return defaultPath; } - }); - // Adds the general default extension - exts.push('.md'); - - // Choose the first non null - return exts.find(function(e) { return e !== null; }); -}; + } -/** - 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(absolute) { - const 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} + */ + getDefaultSummaryPath(absolute) { + const defaultPath = 'SUMMARY' + 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(absolute) { - const 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} + */ + getDefaultGlossaryPath(absolute) { + const defaultPath = 'GLOSSARY' + 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(absolute) { - const defaultPath = 'GLOSSARY' + this.getDefaultExt(); - if (absolute) { - return path.join(this.getContentRoot(), defaultPath); - } else { - return defaultPath; + /** + * Create a language book from a parent + * + * @param {Book} parent + * @param {String} language + * @return {Book} + */ + static createFromParent(parent, language) { + const ignore = parent.getIgnore(); + let 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, + ignore, + + language, + fs: FS.reduceScope(parent.getContentFS(), language) + }); } -}; -/** - Create a language book from a parent - - @param {Book} parent - @param {String} language - @return {Book} -*/ -Book.createFromParent = function createFromParent(parent, language) { - const ignore = parent.getIgnore(); - let 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, - ignore, - - language, - fs: FS.reduceScope(parent.getContentFS(), language) - }); -}; + /** + * Create a book using a filesystem + * + * @param {FS} fs + * @return {Book} + */ + static createForFS(fs) { + return new Book({ + fs + }); + } +} module.exports = Book; diff --git a/packages/gitbook/src/models/output.js b/packages/gitbook/src/models/output.js index 55d83a4..ae68f4c 100644 --- a/packages/gitbook/src/models/output.js +++ b/packages/gitbook/src/models/output.js @@ -4,7 +4,7 @@ const Book = require('./book'); const LocationUtils = require('../utils/location'); const Output = Immutable.Record({ - book: Book(), + book: new Book(), // Name of the generator being used generator: String(), diff --git a/packages/gitbook/src/models/uriIndex.js b/packages/gitbook/src/models/uriIndex.js new file mode 100644 index 0000000..32de3cf --- /dev/null +++ b/packages/gitbook/src/models/uriIndex.js @@ -0,0 +1,114 @@ +const path = require('path'); +const url = require('url'); +const { Record, Map } = require('immutable'); +const LocationUtils = require('../utils/location'); + +/* + The URIIndex stores a map of filename to url. + To resolve urls for each article. + */ + +const DEFAULTS = { + uris: Map() +}; + +/** + * Modify an url path while preserving the hash + * @param {String} input + * @param {Function<String>} transform + * @return {String} output + */ +function transformURLPath(input, transform) { + // Split anchor + const parsed = url.parse(input); + input = parsed.pathname || ''; + + input = transform(input); + + // Add back anchor + input = input + (parsed.hash || ''); + + return input; +} + +class URIIndex extends Record(DEFAULTS) { + constructor(index) { + super({ + uris: Map(index) + .mapKeys(key => LocationUtils.normalize(key)) + }); + } + + /** + * Append a file to the index + * @param {String} filePath + * @param {String} url + * @return {URIIndex} + */ + append(filePath, uri) { + const { uris } = this; + filePath = LocationUtils.normalize(filePath); + + return this.merge({ + uris: uris.set(filePath, uri) + }); + } + + /** + * Resolve an absolute file path to an url. + * + * @param {String} filePath + * @return {String} url + */ + resolve(filePath) { + if (LocationUtils.isExternal(filePath)) { + return filePath; + } + + return transformURLPath(filePath, (href) => { + const { uris } = this; + href = LocationUtils.normalize(href); + + return uris.get(href, href); + }); + } + + /** + * Resolve a filename to an url, considering that the link to "filePath" + * in the file "originPath". + * + * For example if we are generating doc/README.md and we have a link "/READNE.md": + * index.resolveFrom('doc/README.md', '/README.md') === '../index.html' + * + * @param {String} filePath + * @return {String} url + */ + resolveFrom(originPath, filePath) { + if (LocationUtils.isExternal(filePath)) { + return filePath; + } + + const originURL = this.resolve(originPath); + const originDir = path.dirname(originPath); + const originOutDir = path.dirname(originURL); + + return transformURLPath(filePath, (href) => { + if (!href) { + return href; + } + // Calcul absolute path for this + href = LocationUtils.toAbsolute(href, originDir, '.'); + + // Resolve file + href = this.resolve(href); + + // Convert back to relative + href = LocationUtils.relative(originOutDir, href); + + return href; + }); + } + +} + +module.exports = URIIndex; |