summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api/README.md3
-rw-r--r--docs/api/node.md97
-rw-r--r--packages/gitbook-core/src/bootstrap.js28
-rw-r--r--packages/gitbook-core/src/components/Import.js37
-rw-r--r--packages/gitbook-core/src/components/InjectedComponent.js8
-rw-r--r--packages/gitbook-core/src/components/UnsafeComponent.js99
-rw-r--r--packages/gitbook-core/src/index.js8
-rw-r--r--packages/gitbook-core/src/lib/getPayload.js19
-rw-r--r--packages/gitbook-core/src/renderWithStore.js18
-rw-r--r--packages/gitbook-plugin-theme-default/src/Toolbar.js15
-rw-r--r--packages/gitbook-plugin-theme-default/src/index.js6
-rw-r--r--packages/gitbook/src/browser/render.js34
-rw-r--r--packages/gitbook/src/output/website/copyPluginAssets.js60
13 files changed, 298 insertions, 134 deletions
diff --git a/docs/api/README.md b/docs/api/README.md
new file mode 100644
index 0000000..4349fd4
--- /dev/null
+++ b/docs/api/README.md
@@ -0,0 +1,3 @@
+# Plugin Architecture
+
+A GitBook plugin is a NPM package that follow a defined convention.
diff --git a/docs/api/node.md b/docs/api/node.md
new file mode 100644
index 0000000..99bf1a7
--- /dev/null
+++ b/docs/api/node.md
@@ -0,0 +1,97 @@
+# Context and APIs
+
+GitBooks provides different Node APIs and contexts to plugins. These APIs can vary according to the GitBook version being used, your plugin should specify the `engines.gitbook` field in `package.json` accordingly.
+
+#### Book instance
+
+The `Book` interface is the central point of GitBook, it centralize all access read methods.
+
+```js
+// Read configuration from book.json
+var value = book.config.get('title', 'Default Value');
+
+// Resolve a filename to an absolute path
+var filepath = book.resolve('README.md');
+
+// Render an inline markup string
+book.renderInline('markdown', 'This is **Markdown**')
+ .then(function(str) { ... })
+
+// Render a markup string (block mode)
+book.renderBlock('markdown', '* This is **Markdown**')
+ .then(function(str) { ... })
+```
+
+#### Output instance
+
+The `Output` class represent the output/write process.
+
+```js
+// Return root folder for the output
+var root = output.root();
+
+// Resolve a file in the output folder
+var filepath = output.resolve('myimage.png');
+
+// Convert a filename to an URL (returns a path to an html file)
+var fileurl = output.toURL('mychapter/README.md');
+
+// Write a file in the output folder
+output.writeFile('hello.txt', 'Hello World')
+ .then(function() { ... });
+
+// Copy a file to the output folder
+output.copyFile('./myfile.jpg', 'cover.jpg')
+ .then(function() { ... });
+
+// Verify that a file exists
+output.hasFile('hello.txt')
+ .then(function(exists) { ... });
+```
+
+#### Page instance
+
+A page instance represent the current parsed page.
+
+```js
+// Title of the page (from SUMMARY)
+page.title
+
+// Content of the page (Markdown/Asciidoc/HTML according to the stage)
+page.content
+
+// Relative path in the book
+page.path
+
+// Absolute path to the file
+page.rawPath
+
+// Type of parser used for this file
+page.type ('markdown' or 'asciidoc')
+```
+
+#### Context for Blocks and Filters
+
+Blocks and filters have access to the same context, this context is bind to the template engine execution:
+
+```js
+{
+ // Current templating syntax
+ "ctx": {
+ // For example, after a {% set message = "hello" %}
+ "message": "hello"
+ },
+
+ // Book instance
+ "book" <Book>,
+
+ // Output instance
+ "output": <Output>
+}
+```
+
+For example a filter or block function can access the current book using: `this.book`.
+
+#### Context for Hooks
+
+Hooks only have access to the `<Book>` instance using `this.book`.
diff --git a/packages/gitbook-core/src/bootstrap.js b/packages/gitbook-core/src/bootstrap.js
new file mode 100644
index 0000000..a5b457b
--- /dev/null
+++ b/packages/gitbook-core/src/bootstrap.js
@@ -0,0 +1,28 @@
+const ReactDOM = require('react-dom');
+
+const getPayload = require('./lib/getPayload');
+const createStore = require('./createStore');
+const renderWithStore = require('./renderWithStore');
+
+/**
+ * Bootstrap GitBook on the browser (this function should not be called on the server side)
+ */
+function bootstrap() {
+ const initialState = getPayload(window.document);
+ const plugins = window.gitbookPlugins;
+
+ const mountNode = document.getElementById('content');
+
+ // Create the redux store
+ const store = createStore(plugins, initialState);
+
+ window.appStore = store;
+
+ // Render with the store
+ const el = renderWithStore(store);
+
+ ReactDOM.render(el, mountNode);
+}
+
+
+module.exports = bootstrap;
diff --git a/packages/gitbook-core/src/components/Import.js b/packages/gitbook-core/src/components/Import.js
new file mode 100644
index 0000000..057ef7a
--- /dev/null
+++ b/packages/gitbook-core/src/components/Import.js
@@ -0,0 +1,37 @@
+const React = require('react');
+const Head = require('react-helmet');
+const ReactRedux = require('react-redux');
+
+const ImportLink = ReactRedux.connect((state, {rel, href}) => {
+ href = href; // TODO: resolve using current page
+
+ return {
+ link: [
+ {
+ rel,
+ href
+ }
+ ]
+ };
+})(Head);
+
+const ImportScript = ReactRedux.connect((state, {type, src}) => {
+ src = src; // TODO: resolve using current page
+
+ return {
+ script: [
+ {
+ type,
+ src
+ }
+ ]
+ };
+})(Head);
+
+const ImportCSS = props => <ImportLink rel="stylesheet" {...props} />;
+
+module.exports = {
+ ImportLink,
+ ImportScript,
+ ImportCSS
+};
diff --git a/packages/gitbook-core/src/components/InjectedComponent.js b/packages/gitbook-core/src/components/InjectedComponent.js
index a4f81fe..cb50e02 100644
--- a/packages/gitbook-core/src/components/InjectedComponent.js
+++ b/packages/gitbook-core/src/components/InjectedComponent.js
@@ -2,7 +2,6 @@ const React = require('react');
const ReactRedux = require('react-redux');
const { List } = require('immutable');
-const UnsafeComponent = require('./UnsafeComponent');
const { findMatchingComponents } = require('../actions/components');
/*
@@ -37,11 +36,8 @@ const Injection = React.createClass({
const Comp = this.props.component;
const { props, children } = this.props;
- if (Comp.sandbox === false) {
- return <Comp {...props}>{children}</Comp>;
- } else {
- return <UnsafeComponent Component={Comp} props={props}>{children}</UnsafeComponent>;
- }
+ // TODO: try to render with an error handling for unsafe component
+ return <Comp {...props}>{children}</Comp>;
}
});
diff --git a/packages/gitbook-core/src/components/UnsafeComponent.js b/packages/gitbook-core/src/components/UnsafeComponent.js
deleted file mode 100644
index 3534f45..0000000
--- a/packages/gitbook-core/src/components/UnsafeComponent.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* eslint-disable no-console */
-const React = require('react');
-const ReactDOM = require('react-dom');
-const ReactRedux = require('react-redux');
-
-const isServerSide = typeof window === 'undefined';
-
-/*
- Public: Renders a component provided via the `component` prop, and ensures that
- failures in the component's code do not cause state inconsistencies elsewhere in
- the application. This component is used by {InjectedComponent} and
- {InjectedComponentSet} to isolate third party code that could be buggy.
-
- Occasionally, having your component wrapped in {UnsafeComponent} can cause style
- issues. For example, in a Flexbox, the `div.unsafe-component-wrapper` will cause
- your `flex` and `order` values to be one level too deep. For these scenarios,
- UnsafeComponent looks for `containerStyles` on your React component and attaches
- them to the wrapper div.
- */
-
-
-const UnsafeComponent = React.createClass({
- propTypes: {
- Component: React.PropTypes.func.isRequired,
- props: React.PropTypes.object,
- children: React.PropTypes.node
- },
- contextTypes: {
- store: React.PropTypes.object
- },
-
- componentDidMount() {
- return this.renderInjected();
- },
-
- componentDidUpdate() {
- return this.renderInjected();
- },
-
- componentWillUnmount() {
- return this.unmountInjected();
- },
-
- getInjected() {
- const { Component, props, children } = this.props;
- const { store } = this.context;
-
- return (
- <ReactRedux.Provider store={store}>
- <Component {...props}>{children}</Component>
- </ReactRedux.Provider>
- );
- },
-
- renderInjected() {
- const node = ReactDOM.findDOMNode(this);
-
- try {
- this.injected = this.getInjected();
-
- ReactDOM.render(this.injected, node);
- } catch (err) {
- console.error(err);
- }
- },
-
- unmountInjected() {
- try {
- const node = ReactDOM.findDOMNode(this);
- return ReactDOM.unmountComponentAtNode(node);
- } catch (err) {
- console.error(err);
- }
- },
-
- focus() {
- if (this.injected.focus != null) {
- return this.injected.focus();
- }
- },
-
- blur() {
- if (this.injected.blur != null) {
- return this.injected.blur();
- }
- },
-
- render() {
- let inner;
-
- if (isServerSide) {
- inner = this.getInjected();
- }
-
- return <div name="unsafe-component-wrapper">{inner}</div>;
- }
-});
-
-module.exports = ReactRedux.connect()(UnsafeComponent);
diff --git a/packages/gitbook-core/src/index.js b/packages/gitbook-core/src/index.js
index b4b2abd..46892e4 100644
--- a/packages/gitbook-core/src/index.js
+++ b/packages/gitbook-core/src/index.js
@@ -2,6 +2,7 @@ const Head = require('react-helmet');
const { Provider } = require('react-redux');
const { InjectedComponent, InjectedComponentSet } = require('./components/InjectedComponent');
+const { ImportLink, ImportScript, ImportCSS } = require('./components/Import');
const HTMLContent = require('./components/HTMLContent');
const { registerComponent } = require('./actions/components');
@@ -12,9 +13,13 @@ const connect = require('./connect');
const createPlugin = require('./createPlugin');
const createReducer = require('./createReducer');
const createStore = require('./createStore');
+const bootstrap = require('./bootstrap');
+const renderWithStore = require('./renderWithStore');
module.exports = {
ACTIONS,
+ bootstrap,
+ renderWithStore,
connect,
createPlugin,
createReducer,
@@ -26,6 +31,9 @@ module.exports = {
HTMLContent,
Head,
Provider,
+ ImportLink,
+ ImportScript,
+ ImportCSS,
// Utilities
Shapes
};
diff --git a/packages/gitbook-core/src/lib/getPayload.js b/packages/gitbook-core/src/lib/getPayload.js
new file mode 100644
index 0000000..2d54b9e
--- /dev/null
+++ b/packages/gitbook-core/src/lib/getPayload.js
@@ -0,0 +1,19 @@
+
+/**
+ * Get the payload for a GitBook page
+ * @param {String|DOMDocument} html
+ * @return {Object}
+ */
+function getPayload(html) {
+ if (typeof html === 'string') {
+ const parser = new DOMParser();
+ html = parser.parseFromString(html, 'text/html');
+ }
+
+ const script = html.querySelector('script[type="application/payload+json"]');
+ const payload = JSON.parse(script.innerHTML);
+
+ return payload;
+}
+
+module.exports = getPayload;
diff --git a/packages/gitbook-core/src/renderWithStore.js b/packages/gitbook-core/src/renderWithStore.js
new file mode 100644
index 0000000..af86704
--- /dev/null
+++ b/packages/gitbook-core/src/renderWithStore.js
@@ -0,0 +1,18 @@
+const React = require('react');
+const { Provider } = require('react-redux');
+const { InjectedComponent } = require('./components/InjectedComponent');
+
+/**
+ * Render the application for a store
+ * @param {ReduxStore} store
+ * @return {React.Element} element
+ */
+function renderWithStore(store) {
+ return (
+ <Provider store={store}>
+ <InjectedComponent matching={{ role: 'Body' }} />
+ </Provider>
+ );
+}
+
+module.exports = renderWithStore;
diff --git a/packages/gitbook-plugin-theme-default/src/Toolbar.js b/packages/gitbook-plugin-theme-default/src/Toolbar.js
new file mode 100644
index 0000000..b3fd059
--- /dev/null
+++ b/packages/gitbook-plugin-theme-default/src/Toolbar.js
@@ -0,0 +1,15 @@
+const React = require('react');
+const GitBook = require('gitbook-core');
+
+const Toolbar = React.createClass({
+ render() {
+ return (
+ <div className="Toolbar book-toolbar">
+ <GitBook.InjectedComponentSet matching={{ role: 'toolbar:buttons:left' }} />
+ <GitBook.InjectedComponentSet matching={{ role: 'toolbar:buttons:right' }} />
+ </div>
+ );
+ }
+});
+
+module.exports = Toolbar;
diff --git a/packages/gitbook-plugin-theme-default/src/index.js b/packages/gitbook-plugin-theme-default/src/index.js
index 6499d5a..4b98a22 100644
--- a/packages/gitbook-plugin-theme-default/src/index.js
+++ b/packages/gitbook-plugin-theme-default/src/index.js
@@ -3,6 +3,7 @@ const GitBook = require('gitbook-core');
const Sidebar = require('./Sidebar');
const Page = require('./Page');
+const Toolbar = require('./Toolbar');
let ThemeBody = React.createClass({
propTypes: {
@@ -17,9 +18,10 @@ let ThemeBody = React.createClass({
<div className="GitBook book">
<GitBook.Head
title={page.title}
- titleTemplate="%s - GitBook"
- />
+ titleTemplate="%s - GitBook" />
+ <GitBook.ImportCSS href="gitbook/gitbook-plugin-theme-default/theme.css" />
+ <Toolbar />
<Sidebar />
<Page page={page} />
{children}
diff --git a/packages/gitbook/src/browser/render.js b/packages/gitbook/src/browser/render.js
index ee4600a..3696eba 100644
--- a/packages/gitbook/src/browser/render.js
+++ b/packages/gitbook/src/browser/render.js
@@ -4,7 +4,9 @@ const GitBook = require('gitbook-core');
const loadPlugins = require('./loadPlugins');
-function HTML({head, innerHTML}) {
+const BOOTSTRAP_CODE = '(function() { require("gitbook-core").bootstrap() })()';
+
+function HTML({head, innerHTML, payload, scripts}) {
const attrs = head.htmlAttributes.toComponent();
return (
@@ -17,6 +19,11 @@ function HTML({head, innerHTML}) {
</head>
<body>
<div id="content" dangerouslySetInnerHTML={{__html: innerHTML}} />
+ {scripts.map(script => {
+ return <script key={script} src={script} />;
+ })}
+ <script type="application/payload+json" dangerouslySetInnerHTML={{__html: JSON.stringify(payload)}} />
+ <script type="application/javascript" dangerouslySetInnerHTML={{__html: BOOTSTRAP_CODE}} />
{head.script.toComponent()}
</body>
</html>
@@ -24,7 +31,9 @@ function HTML({head, innerHTML}) {
}
HTML.propTypes = {
head: React.PropTypes.object,
- innerHTML: React.PropTypes.string
+ innerHTML: React.PropTypes.string,
+ payload: React.PropTypes.object,
+ scripts: React.PropTypes.arrayOf(React.PropTypes.string)
};
/**
@@ -40,25 +49,10 @@ function render(plugins, initialState) {
const scripts = plugins.toList()
.filter(plugin => plugin.getPackage().has('browser'))
- .map(plugin => {
- return { src: '/gitbook/plugins/' + plugin.getName() + '.js' };
- })
+ .map(plugin => 'gitbook/plugins/' + plugin.getName() + '.js')
.toArray();
- scripts.push({
- type: 'application/payload+json',
- innerHTML: JSON.stringify(initialState)
- });
-
- const el = (
- <GitBook.Provider store={store}>
- <GitBook.InjectedComponent matching={{ role: 'Body' }}>
- <GitBook.Head
- script={scripts}
- />
- </GitBook.InjectedComponent>
- </GitBook.Provider>
- );
+ const el = GitBook.renderWithStore(store);
// Render inner body
const innerHTML = ReactDOMServer.renderToString(el);
@@ -70,6 +64,8 @@ function render(plugins, initialState) {
const htmlEl = <HTML
head={head}
innerHTML={innerHTML}
+ payload={initialState}
+ scripts={['gitbook/core.js'].concat(scripts)}
/>;
const html = ReactDOMServer.renderToStaticMarkup(htmlEl);
diff --git a/packages/gitbook/src/output/website/copyPluginAssets.js b/packages/gitbook/src/output/website/copyPluginAssets.js
index 312d3d6..1291a06 100644
--- a/packages/gitbook/src/output/website/copyPluginAssets.js
+++ b/packages/gitbook/src/output/website/copyPluginAssets.js
@@ -6,8 +6,7 @@ const fs = require('../../utils/fs');
/**
* Copy all assets from plugins.
- * Assets are files stored in "_assets"
- * and resources declared in the plugin itself.
+ * Assets are files stored in a "_assets" of the plugin.
*
* @param {Output}
* @return {Promise}
@@ -23,16 +22,16 @@ function copyPluginAssets(output) {
const plugins = output.getPlugins()
- // We reverse the order of plugins to copy
- // so that first plugins can replace assets from other plugins.
- .reverse();
+ // We reverse the order of plugins to copy
+ // so that first plugins can replace assets from other plugins.
+ .reverse();
return Promise.forEach(plugins, function(plugin) {
return copyAssets(output, plugin)
- .then(function() {
- return copyResources(output, plugin);
- });
+ .then(() => copyResources(output, plugin))
+ .then(() => copyBrowserJS(output, plugin));
})
+ .then(() => copyCoreJS(output))
.thenResolve(output);
}
@@ -70,6 +69,51 @@ function copyAssets(output, plugin) {
}
/**
+ * Copy JS file for the plugin
+ *
+ * @param {Plugin}
+ * @return {Promise}
+ */
+function copyBrowserJS(output, plugin) {
+ const logger = output.getLogger();
+ const pluginRoot = plugin.getPath();
+ const options = output.getOptions();
+ const outputRoot = options.get('root');
+
+ let browserFile = plugin.getPackage().get('browser');
+
+ if (!browserFile) {
+ return Promise();
+ }
+
+ browserFile = path.join(pluginRoot, browserFile);
+ const outputFile = path.join(outputRoot, 'gitbook/plugins', plugin.getName() + '.js');
+
+ logger.debug.ln('copy browser JS file from plugin', browserFile);
+ return fs.ensureFile(outputFile)
+ .then(() => fs.copy(browserFile, outputFile));
+}
+
+/**
+ * Copy JS file for gitbook-core
+ *
+ * @param {Plugin}
+ * @return {Promise}
+ */
+function copyCoreJS(output) {
+ const logger = output.getLogger();
+ const options = output.getOptions();
+ const outputRoot = options.get('root');
+
+ const inputFile = require.resolve('gitbook-core/gitbook.core.min.js');
+ const outputFile = path.join(outputRoot, 'gitbook/core.js');
+
+ logger.debug.ln('copy JS for gitbook-core');
+ return fs.ensureFile(outputFile)
+ .then(() => fs.copy(inputFile, outputFile));
+}
+
+/**
* Copy resources from a plugin
*
* @param {Plugin}