diff options
-rw-r--r-- | docs/api/README.md | 3 | ||||
-rw-r--r-- | docs/api/node.md | 97 | ||||
-rw-r--r-- | packages/gitbook-core/src/bootstrap.js | 28 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Import.js | 37 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/InjectedComponent.js | 8 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/UnsafeComponent.js | 99 | ||||
-rw-r--r-- | packages/gitbook-core/src/index.js | 8 | ||||
-rw-r--r-- | packages/gitbook-core/src/lib/getPayload.js | 19 | ||||
-rw-r--r-- | packages/gitbook-core/src/renderWithStore.js | 18 | ||||
-rw-r--r-- | packages/gitbook-plugin-theme-default/src/Toolbar.js | 15 | ||||
-rw-r--r-- | packages/gitbook-plugin-theme-default/src/index.js | 6 | ||||
-rw-r--r-- | packages/gitbook/src/browser/render.js | 34 | ||||
-rw-r--r-- | packages/gitbook/src/output/website/copyPluginAssets.js | 60 |
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} |