summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamy Pesse <samypesse@gmail.com>2016-10-06 11:21:08 +0200
committerSamy Pesse <samypesse@gmail.com>2016-10-06 11:21:08 +0200
commit4119917555f827b0bff256a8e34a1deef5f4b87e (patch)
tree2304bd9587340dad1a0d35a077fbd43c1aaaa34b
parent9d20a9afa5603bcc703c6787c2ad41d124997fab (diff)
downloadgitbook-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.js71
-rw-r--r--packages/gitbook/src/models/book.js598
-rw-r--r--packages/gitbook/src/models/output.js2
-rw-r--r--packages/gitbook/src/models/uriIndex.js114
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;