summaryrefslogtreecommitdiffstats
path: root/packages/gitbook-core
diff options
context:
space:
mode:
authorSamy Pessé <samypesse@gmail.com>2016-12-22 10:18:38 +0100
committerGitHub <noreply@github.com>2016-12-22 10:18:38 +0100
commit194ebc3da9641ff96f083f9d8ab43c2d27944f9a (patch)
treec50988f32ccf18df93ae7ab40be78e9459642818 /packages/gitbook-core
parent64ccb6b00b4b63fa0e516d4e35351275b34f8c07 (diff)
parent16af264360e48e8a833e9efa9ab8d194574dbc70 (diff)
downloadgitbook-194ebc3da9641ff96f083f9d8ab43c2d27944f9a.zip
gitbook-194ebc3da9641ff96f083f9d8ab43c2d27944f9a.tar.gz
gitbook-194ebc3da9641ff96f083f9d8ab43c2d27944f9a.tar.bz2
Merge pull request #1543 from GitbookIO/dream
React for rendering website with plugins
Diffstat (limited to 'packages/gitbook-core')
-rw-r--r--packages/gitbook-core/.babelrc3
-rw-r--r--packages/gitbook-core/.gitignore1
-rw-r--r--packages/gitbook-core/.npmignore3
-rw-r--r--packages/gitbook-core/package.json51
-rw-r--r--packages/gitbook-core/src/actions/TYPES.js16
-rw-r--r--packages/gitbook-core/src/actions/components.js37
-rw-r--r--packages/gitbook-core/src/actions/history.js188
-rw-r--r--packages/gitbook-core/src/actions/i18n.js33
-rw-r--r--packages/gitbook-core/src/components/Backdrop.js56
-rw-r--r--packages/gitbook-core/src/components/Button.js22
-rw-r--r--packages/gitbook-core/src/components/ButtonGroup.js23
-rw-r--r--packages/gitbook-core/src/components/ContextProvider.js34
-rw-r--r--packages/gitbook-core/src/components/Dropdown.js126
-rw-r--r--packages/gitbook-core/src/components/HTMLContent.js77
-rw-r--r--packages/gitbook-core/src/components/HotKeys.js59
-rw-r--r--packages/gitbook-core/src/components/I18nProvider.js28
-rw-r--r--packages/gitbook-core/src/components/Icon.js28
-rw-r--r--packages/gitbook-core/src/components/Import.js48
-rw-r--r--packages/gitbook-core/src/components/InjectedComponent.js117
-rw-r--r--packages/gitbook-core/src/components/Link.js37
-rw-r--r--packages/gitbook-core/src/components/PJAXWrapper.js102
-rw-r--r--packages/gitbook-core/src/components/Panel.js22
-rw-r--r--packages/gitbook-core/src/components/Tooltipped.js44
-rw-r--r--packages/gitbook-core/src/index.js73
-rw-r--r--packages/gitbook-core/src/lib/bootstrap.js29
-rw-r--r--packages/gitbook-core/src/lib/composeReducer.js16
-rw-r--r--packages/gitbook-core/src/lib/connect.js70
-rw-r--r--packages/gitbook-core/src/lib/createContext.js76
-rw-r--r--packages/gitbook-core/src/lib/createPlugin.js27
-rw-r--r--packages/gitbook-core/src/lib/createReducer.js27
-rw-r--r--packages/gitbook-core/src/lib/getPayload.js19
-rw-r--r--packages/gitbook-core/src/lib/renderWithContext.js55
-rw-r--r--packages/gitbook-core/src/models/Context.js58
-rw-r--r--packages/gitbook-core/src/models/File.js54
-rw-r--r--packages/gitbook-core/src/models/Language.js12
-rw-r--r--packages/gitbook-core/src/models/Languages.js40
-rw-r--r--packages/gitbook-core/src/models/Location.js78
-rw-r--r--packages/gitbook-core/src/models/Page.js24
-rw-r--r--packages/gitbook-core/src/models/Plugin.js21
-rw-r--r--packages/gitbook-core/src/models/Readme.js21
-rw-r--r--packages/gitbook-core/src/models/SummaryArticle.js32
-rw-r--r--packages/gitbook-core/src/models/SummaryPart.js17
-rw-r--r--packages/gitbook-core/src/propTypes/Context.js11
-rw-r--r--packages/gitbook-core/src/propTypes/File.js13
-rw-r--r--packages/gitbook-core/src/propTypes/History.js11
-rw-r--r--packages/gitbook-core/src/propTypes/Language.js7
-rw-r--r--packages/gitbook-core/src/propTypes/Languages.js12
-rw-r--r--packages/gitbook-core/src/propTypes/Location.js12
-rw-r--r--packages/gitbook-core/src/propTypes/Page.js16
-rw-r--r--packages/gitbook-core/src/propTypes/Readme.js11
-rw-r--r--packages/gitbook-core/src/propTypes/Summary.js14
-rw-r--r--packages/gitbook-core/src/propTypes/SummaryArticle.js22
-rw-r--r--packages/gitbook-core/src/propTypes/SummaryPart.js14
-rw-r--r--packages/gitbook-core/src/propTypes/i18n.js10
-rw-r--r--packages/gitbook-core/src/propTypes/index.js19
-rw-r--r--packages/gitbook-core/src/reducers/components.js20
-rw-r--r--packages/gitbook-core/src/reducers/config.js15
-rw-r--r--packages/gitbook-core/src/reducers/file.js16
-rw-r--r--packages/gitbook-core/src/reducers/history.js82
-rw-r--r--packages/gitbook-core/src/reducers/i18n.js27
-rw-r--r--packages/gitbook-core/src/reducers/index.js15
-rw-r--r--packages/gitbook-core/src/reducers/languages.js12
-rw-r--r--packages/gitbook-core/src/reducers/page.js16
-rw-r--r--packages/gitbook-core/src/reducers/readme.js5
-rw-r--r--packages/gitbook-core/src/reducers/summary.js28
-rw-r--r--packages/gitbook-core/src/server.js2
66 files changed, 2314 insertions, 0 deletions
diff --git a/packages/gitbook-core/.babelrc b/packages/gitbook-core/.babelrc
new file mode 100644
index 0000000..5f27bda
--- /dev/null
+++ b/packages/gitbook-core/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015", "react", "stage-2"]
+}
diff --git a/packages/gitbook-core/.gitignore b/packages/gitbook-core/.gitignore
new file mode 100644
index 0000000..eed58d7
--- /dev/null
+++ b/packages/gitbook-core/.gitignore
@@ -0,0 +1 @@
+gitbook.core.min.js
diff --git a/packages/gitbook-core/.npmignore b/packages/gitbook-core/.npmignore
new file mode 100644
index 0000000..b9cde8e
--- /dev/null
+++ b/packages/gitbook-core/.npmignore
@@ -0,0 +1,3 @@
+src
+!lib
+!dist
diff --git a/packages/gitbook-core/package.json b/packages/gitbook-core/package.json
new file mode 100644
index 0000000..182e6e3
--- /dev/null
+++ b/packages/gitbook-core/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "gitbook-core",
+ "version": "4.0.0",
+ "description": "Core for GitBook plugins API",
+ "main": "./lib/index.js",
+ "dependencies": {
+ "bluebird": "^3.4.6",
+ "classnames": "^2.2.5",
+ "entities": "^1.1.1",
+ "history": "^4.3.0",
+ "html-tags": "^1.1.1",
+ "immutable": "^3.8.1",
+ "mousetrap": "1.6.0",
+ "react": "15.4.1",
+ "react-addons-css-transition-group": "15.4.1",
+ "react-dom": "15.4.1",
+ "react-helmet": "^3.1.0",
+ "react-immutable-proptypes": "^2.1.0",
+ "react-intl": "^2.1.5",
+ "react-redux": "^4.4.5",
+ "react-safe-html": "0.4.0",
+ "redux": "^3.5.2",
+ "redux-thunk": "^2.1.0",
+ "reflexbox": "^2.2.2",
+ "whatwg-fetch": "^1.0.0"
+ },
+ "devDependencies": {
+ "babel-cli": "^6.14.0",
+ "babel-preset-es2015": "^6.14.0",
+ "babel-preset-react": "^6.11.1",
+ "babel-preset-stage-2": "^6.13.0",
+ "browserify": "^13.1.0",
+ "envify": "^3.4.1",
+ "uglify-js": "^2.7.3"
+ },
+ "scripts": {
+ "dist-lib": "rm -rf lib/ && babel -d lib/ src/",
+ "dist-standalone": "mkdir -p dist && browserify -r ./lib/index.js:gitbook-core -r react -r react-dom ./lib/index.js | uglifyjs -c > ./dist/gitbook.core.min.js",
+ "dist": "npm run dist-lib && npm run dist-standalone",
+ "prepublish": "npm run dist"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/GitbookIO/gitbook.git"
+ },
+ "author": "GitBook Inc. <contact@gitbook.com>",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/GitbookIO/gitbook/issues"
+ }
+}
diff --git a/packages/gitbook-core/src/actions/TYPES.js b/packages/gitbook-core/src/actions/TYPES.js
new file mode 100644
index 0000000..9876057
--- /dev/null
+++ b/packages/gitbook-core/src/actions/TYPES.js
@@ -0,0 +1,16 @@
+
+module.exports = {
+ // Components
+ REGISTER_COMPONENT: 'components/register',
+ UNREGISTER_COMPONENT: 'components/unregister',
+ // Navigation
+ HISTORY_ACTIVATE: 'history/activate',
+ HISTORY_DEACTIVATE: 'history/deactivate',
+ HISTORY_LISTEN: 'history/listen',
+ HISTORY_UPDATE: 'history/update',
+ PAGE_FETCH_START: 'history/fetch:start',
+ PAGE_FETCH_END: 'history/fetch:end',
+ PAGE_FETCH_ERROR: 'history/fetch:error',
+ // i18n
+ I18N_REGISTER_LOCALE: 'i18n/register:locale'
+};
diff --git a/packages/gitbook-core/src/actions/components.js b/packages/gitbook-core/src/actions/components.js
new file mode 100644
index 0000000..f21c382
--- /dev/null
+++ b/packages/gitbook-core/src/actions/components.js
@@ -0,0 +1,37 @@
+const ACTION_TYPES = require('./TYPES');
+
+/**
+ * Find all components matching a descriptor
+ * @param {List<ComponentDescriptor>} state
+ * @param {String} matching.role
+ */
+function findMatchingComponents(state, matching) {
+ return state
+ .filter(({descriptor}) => {
+ if (matching.role && matching.role !== descriptor.role) {
+ return false;
+ }
+
+ return true;
+ })
+ .map(component => component.Component);
+}
+
+/**
+ * Register a new component
+ * @param {React.Class} Component
+ * @param {Descriptor} descriptor
+ * @return {Action}
+ */
+function registerComponent(Component, descriptor) {
+ return {
+ type: ACTION_TYPES.REGISTER_COMPONENT,
+ Component,
+ descriptor
+ };
+}
+
+module.exports = {
+ findMatchingComponents,
+ registerComponent
+};
diff --git a/packages/gitbook-core/src/actions/history.js b/packages/gitbook-core/src/actions/history.js
new file mode 100644
index 0000000..1c33f4a
--- /dev/null
+++ b/packages/gitbook-core/src/actions/history.js
@@ -0,0 +1,188 @@
+const ACTION_TYPES = require('./TYPES');
+const getPayload = require('../lib/getPayload');
+const Location = require('../models/Location');
+
+const SUPPORTED = (
+ typeof window !== 'undefined' &&
+ window.history && window.history.pushState && window.history.replaceState &&
+ // pushState isn't reliable on iOS until 5.
+ !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
+);
+
+/**
+ * Initialize the history
+ */
+function activate() {
+ return (dispatch, getState) => {
+ dispatch({
+ type: ACTION_TYPES.HISTORY_ACTIVATE,
+ listener: (location) => {
+ location = Location.fromNative(location);
+ const prevLocation = getState().history.location;
+
+ // Fetch page if required
+ if (!prevLocation || location.pathname !== prevLocation.pathname) {
+ dispatch(fetchPage(location.pathname));
+ }
+
+ // Signal location to listener
+ dispatch(emit());
+
+ // Update the location
+ dispatch({
+ type: ACTION_TYPES.HISTORY_UPDATE,
+ location
+ });
+ }
+ });
+
+ // Trigger for existing listeners
+ dispatch(emit());
+ };
+}
+
+/**
+ * Emit current location
+ * @param {List|Array<Function>} to?
+ */
+function emit(to) {
+ return (dispatch, getState) => {
+ const { listeners, client } = getState().history;
+
+ if (!client) {
+ return;
+ }
+
+ const location = Location.fromNative(client.location);
+
+ to = to || listeners;
+
+ to.forEach(handler => {
+ handler(location, dispatch, getState);
+ });
+ };
+}
+
+/**
+ * Cleanup the history
+ */
+function deactivate() {
+ return { type: ACTION_TYPES.HISTORY_DEACTIVATE };
+}
+
+/**
+ * Push a new url into the history
+ * @param {String|Location} location
+ * @return {Action} action
+ */
+function push(location) {
+ return (dispatch, getState) => {
+ const { client } = getState().history;
+ location = Location.fromNative(location);
+
+ if (SUPPORTED) {
+ client.push(location.toNative());
+ } else {
+ redirect(location.toString());
+ }
+ };
+}
+
+/**
+ * Replace current state in history
+ * @param {String|Location} location
+ * @return {Action} action
+ */
+function replace(location) {
+ return (dispatch, getState) => {
+ const { client } = getState().history;
+ location = Location.fromNative(location);
+
+ if (SUPPORTED) {
+ client.replace(location.toNative());
+ } else {
+ redirect(location.toString());
+ }
+ };
+}
+
+/**
+ * Hard redirection
+ * @param {String} uri
+ * @return {Action} action
+ */
+function redirect(uri) {
+ return () => {
+ window.location.href = uri;
+ };
+}
+
+/**
+ * Listen to url change
+ * @param {Function} listener
+ * @return {Action} action
+ */
+function listen(listener) {
+ return (dispatch, getState) => {
+ dispatch({ type: ACTION_TYPES.HISTORY_LISTEN, listener });
+
+ // Trigger for existing listeners
+ dispatch(emit([ listener ]));
+ };
+}
+
+/**
+ * Fetch a new page and update the store accordingly
+ * @param {String} pathname
+ * @return {Action} action
+ */
+function fetchPage(pathname) {
+ return (dispatch, getState) => {
+ dispatch({ type: ACTION_TYPES.PAGE_FETCH_START });
+
+ window.fetch(pathname, {
+ credentials: 'include'
+ })
+ .then(
+ response => {
+ return response.text();
+ }
+ )
+ .then(
+ html => {
+ const payload = getPayload(html);
+
+ if (!payload) {
+ throw new Error('No payload found in page');
+ }
+
+ dispatch({ type: ACTION_TYPES.PAGE_FETCH_END, payload });
+ }
+ )
+ .catch(
+ error => {
+ // dispatch(redirect(pathname));
+ dispatch({ type: ACTION_TYPES.PAGE_FETCH_ERROR, error });
+ }
+ );
+ };
+}
+
+/**
+ * Fetch a new article
+ * @param {SummaryArticle} article
+ * @return {Action} action
+ */
+function fetchArticle(article) {
+ return fetchPage(article.path);
+}
+
+module.exports = {
+ activate,
+ deactivate,
+ listen,
+ push,
+ replace,
+ fetchPage,
+ fetchArticle
+};
diff --git a/packages/gitbook-core/src/actions/i18n.js b/packages/gitbook-core/src/actions/i18n.js
new file mode 100644
index 0000000..115c5a1
--- /dev/null
+++ b/packages/gitbook-core/src/actions/i18n.js
@@ -0,0 +1,33 @@
+const ACTION_TYPES = require('./TYPES');
+
+/**
+ * Register messages for a locale
+ * @param {String} locale
+ * @param {Map<String:String>} messages
+ * @return {Action}
+ */
+function registerLocale(locale, messages) {
+ return { type: ACTION_TYPES.I18N_REGISTER_LOCALE, locale, messages };
+}
+
+/**
+ * Register multiple locales
+ * @param {Map<String:Object>} locales
+ * @return {Action}
+ */
+function registerLocales(locales) {
+ return (dispatch) => {
+ for (const locale in locales) {
+ if (!locales.hasOwnProperty(locale)) {
+ continue;
+ }
+
+ dispatch(registerLocale(locale, locales[locale]));
+ }
+ };
+}
+
+module.exports = {
+ registerLocale,
+ registerLocales
+};
diff --git a/packages/gitbook-core/src/components/Backdrop.js b/packages/gitbook-core/src/components/Backdrop.js
new file mode 100644
index 0000000..7b34b0d
--- /dev/null
+++ b/packages/gitbook-core/src/components/Backdrop.js
@@ -0,0 +1,56 @@
+const React = require('react');
+const HotKeys = require('./HotKeys');
+
+/**
+ * Backdrop for modals, dropdown, etc. that covers the whole screen
+ * and handles click and pressing escape.
+ *
+ * <Backdrop onClose={onCloseModal} />
+ */
+const Backdrop = React.createClass({
+ propTypes: {
+ // Callback when backdrop is closed
+ onClose: React.PropTypes.func.isRequired,
+ // Z-index for the backdrop
+ zIndex: React.PropTypes.number,
+ children: React.PropTypes.node
+ },
+
+ getDefaultProps() {
+ return {
+ zIndex: 200
+ };
+ },
+
+ onClick(event) {
+ const { onClose } = this.props;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ onClose();
+ },
+
+ render() {
+ const { zIndex, children, onClose } = this.props;
+ const style = {
+ zIndex,
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ width: '100%',
+ height: '100%'
+ };
+ const keyMap = {
+ 'escape': onClose
+ };
+
+ return (
+ <HotKeys keyMap={keyMap}>
+ <div style={style} onClick={this.onClick}>{children}</div>
+ </HotKeys>
+ );
+ }
+});
+
+module.exports = Backdrop;
diff --git a/packages/gitbook-core/src/components/Button.js b/packages/gitbook-core/src/components/Button.js
new file mode 100644
index 0000000..4d929b8
--- /dev/null
+++ b/packages/gitbook-core/src/components/Button.js
@@ -0,0 +1,22 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const Button = React.createClass({
+ propTypes: {
+ active: React.PropTypes.bool,
+ className: React.PropTypes.string,
+ children: React.PropTypes.node,
+ onClick: React.PropTypes.func
+ },
+
+ render() {
+ const { children, active, onClick } = this.props;
+ const className = classNames('GitBook-Button', this.props.className, {
+ active
+ });
+
+ return <button className={className} onClick={onClick}>{children}</button>;
+ }
+});
+
+module.exports = Button;
diff --git a/packages/gitbook-core/src/components/ButtonGroup.js b/packages/gitbook-core/src/components/ButtonGroup.js
new file mode 100644
index 0000000..4c20b68
--- /dev/null
+++ b/packages/gitbook-core/src/components/ButtonGroup.js
@@ -0,0 +1,23 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const ButtonGroup = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node,
+ onClick: React.PropTypes.func
+ },
+
+ render() {
+ let { className, children } = this.props;
+
+ className = classNames(
+ 'GitBook-ButtonGroup',
+ className
+ );
+
+ return <div className={className}>{children}</div>;
+ }
+});
+
+module.exports = ButtonGroup;
diff --git a/packages/gitbook-core/src/components/ContextProvider.js b/packages/gitbook-core/src/components/ContextProvider.js
new file mode 100644
index 0000000..96a44e3
--- /dev/null
+++ b/packages/gitbook-core/src/components/ContextProvider.js
@@ -0,0 +1,34 @@
+const React = require('react');
+const { Provider } = require('react-redux');
+
+const ContextShape = require('../propTypes/Context');
+
+/**
+ * React component to provide a GitBook context to children components.
+ */
+
+const ContextProvider = React.createClass({
+ propTypes: {
+ context: ContextShape.isRequired,
+ children: React.PropTypes.node
+ },
+
+ childContextTypes: {
+ gitbook: ContextShape
+ },
+
+ getChildContext() {
+ const { context } = this.props;
+
+ return {
+ gitbook: context
+ };
+ },
+
+ render() {
+ const { context, children } = this.props;
+ return <Provider store={context.store}>{children}</Provider>;
+ }
+});
+
+module.exports = ContextProvider;
diff --git a/packages/gitbook-core/src/components/Dropdown.js b/packages/gitbook-core/src/components/Dropdown.js
new file mode 100644
index 0000000..83a377f
--- /dev/null
+++ b/packages/gitbook-core/src/components/Dropdown.js
@@ -0,0 +1,126 @@
+const React = require('react');
+const classNames = require('classnames');
+
+/**
+ * Dropdown to display a menu
+ *
+ * <Dropdown.Container>
+ *
+ * <Button />
+ *
+ * <Dropdown.Menu>
+ * <Dropdown.Item href={...}> ... </Dropdown.Item>
+ * <Dropdown.Item onClick={...}> ... </Dropdown.Item>
+ * </Dropdown.Menu>
+ * </Dropdown.Container>
+ */
+
+const DropdownContainer = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { className, children } = this.props;
+
+ className = classNames(
+ 'GitBook-Dropdown',
+ className
+ );
+
+ return (
+ <div className={className}>
+ {children}
+ </div>
+ );
+ }
+});
+
+/**
+ * A dropdown item which can contains informations.
+ */
+const DropdownItem = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node
+ },
+
+ render() {
+ const { children } = this.props;
+
+ return (
+ <div className="GitBook-DropdownItem">
+ {children}
+ </div>
+ );
+ }
+});
+
+
+/**
+ * A dropdown item, which is always a link.
+ */
+const DropdownItemLink = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node,
+ onClick: React.PropTypes.func,
+ href: React.PropTypes.string
+ },
+
+ onClick(event) {
+ const { onClick, href } = this.props;
+
+ if (href) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (onClick) {
+ onClick();
+ }
+ },
+
+ render() {
+ const { children, href, ...otherProps } = this.props;
+
+ return (
+ <a {...otherProps} className="GitBook-DropdownItemLink" href={href || '#'} onClick={this.onClick} >
+ {children}
+ </a>
+ );
+ }
+});
+
+
+/**
+ * A DropdownMenu to display DropdownItems. Must be inside a
+ * DropdownContainer.
+ */
+const DropdownMenu = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { className, children } = this.props;
+ className = classNames('GitBook-DropdownMenu', className);
+
+ return (
+ <div className={className}>
+ {children}
+ </div>
+ );
+ }
+});
+
+const Dropdown = {
+ Item: DropdownItem,
+ ItemLink: DropdownItemLink,
+ Menu: DropdownMenu,
+ Container: DropdownContainer
+};
+
+module.exports = Dropdown;
diff --git a/packages/gitbook-core/src/components/HTMLContent.js b/packages/gitbook-core/src/components/HTMLContent.js
new file mode 100644
index 0000000..9d15398
--- /dev/null
+++ b/packages/gitbook-core/src/components/HTMLContent.js
@@ -0,0 +1,77 @@
+const React = require('react');
+const ReactSafeHtml = require('react-safe-html');
+const { DOMProperty } = require('react-dom/lib/ReactInjection');
+const htmlTags = require('html-tags');
+const entities = require('entities');
+
+const { InjectedComponent } = require('./InjectedComponent');
+
+DOMProperty.injectDOMPropertyConfig({
+ Properties: {
+ align: DOMProperty.MUST_USE_ATTRIBUTE
+ },
+ isCustomAttribute: (attributeName) => {
+ return attributeName === 'align';
+ }
+});
+
+/*
+ HTMLContent is a container for the page HTML that parse the content and
+ render the right block.
+ All html elements can be extended using the injected component.
+ */
+
+function inject(injectedProps, Component) {
+ return (props) => {
+ const cleanProps = {
+ ...props,
+ className: props['class']
+ };
+ delete cleanProps['class'];
+
+ return (
+ <InjectedComponent {...injectedProps(cleanProps)}>
+ <Component {...cleanProps} />
+ </InjectedComponent>
+ );
+ };
+}
+
+const COMPONENTS = {
+ // Templating blocks are exported as <xblock name="youtube" props="{}" />
+ 'xblock': inject(
+ ({name, props}) => {
+ props = entities.decodeHTML(props);
+ return {
+ matching: { role: `block:${name}` },
+ props: JSON.parse(props)
+ };
+ },
+ props => <div {...props} />
+ )
+};
+
+htmlTags.forEach(tag => {
+ COMPONENTS[tag] = inject(
+ props => {
+ return {
+ matching: { role: `html:${tag}` },
+ props
+ };
+ },
+ props => React.createElement(tag, props)
+ );
+});
+
+const HTMLContent = React.createClass({
+ propTypes: {
+ html: React.PropTypes.string.isRequired
+ },
+
+ render() {
+ const { html } = this.props;
+ return <ReactSafeHtml html={html} components={COMPONENTS} />;
+ }
+});
+
+module.exports = HTMLContent;
diff --git a/packages/gitbook-core/src/components/HotKeys.js b/packages/gitbook-core/src/components/HotKeys.js
new file mode 100644
index 0000000..e2a8154
--- /dev/null
+++ b/packages/gitbook-core/src/components/HotKeys.js
@@ -0,0 +1,59 @@
+const React = require('react');
+const Mousetrap = require('mousetrap');
+const { Map } = require('immutable');
+
+/**
+ * Defines hotkeys globally when this component is mounted.
+ *
+ * keyMap = {
+ * 'escape': (e) => quit()
+ * 'mod+s': (e) => save()
+ * }
+ *
+ * <HotKeys keyMap={keyMap}>
+ * < ... />
+ * </HotKeys>
+ */
+
+const HotKeys = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node.isRequired,
+ keyMap: React.PropTypes.objectOf(React.PropTypes.func)
+ },
+
+ getDefaultProps() {
+ return { keyMap: {} };
+ },
+
+ updateBindings(keyMap) {
+ Map(keyMap).forEach((handler, key) => {
+ Mousetrap.bind(key, handler);
+ });
+ },
+
+ clearBindings(keyMap) {
+ Map(keyMap).forEach((handler, key) => {
+ Mousetrap.unbind(key, handler);
+ });
+ },
+
+ componentDidMount() {
+ this.updateBindings(this.props.keyMap);
+ },
+
+ componentDidUpdate(prevProps) {
+ this.clearBindings(prevProps.keyMap);
+ this.updateBindings(this.props.keyMap);
+ },
+
+ componentWillUnmount() {
+ this.clearBindings(this.props.keyMap);
+ },
+
+ render() {
+ // Simply render the only child
+ return React.Children.only(this.props.children);
+ }
+});
+
+module.exports = HotKeys;
diff --git a/packages/gitbook-core/src/components/I18nProvider.js b/packages/gitbook-core/src/components/I18nProvider.js
new file mode 100644
index 0000000..b6b2d0f
--- /dev/null
+++ b/packages/gitbook-core/src/components/I18nProvider.js
@@ -0,0 +1,28 @@
+const { Map } = require('immutable');
+const React = require('react');
+const intl = require('react-intl');
+const ReactRedux = require('react-redux');
+
+const I18nProvider = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node,
+ messages: React.PropTypes.object
+ },
+
+ render() {
+ let { messages } = this.props;
+ messages = messages.get('en', Map()).toJS();
+
+ return (
+ <intl.IntlProvider locale={'en'} messages={messages}>
+ {this.props.children}
+ </intl.IntlProvider>
+ );
+ }
+});
+
+const mapStateToProps = state => {
+ return { messages: state.i18n.messages };
+};
+
+module.exports = ReactRedux.connect(mapStateToProps)(I18nProvider);
diff --git a/packages/gitbook-core/src/components/Icon.js b/packages/gitbook-core/src/components/Icon.js
new file mode 100644
index 0000000..5f2c751
--- /dev/null
+++ b/packages/gitbook-core/src/components/Icon.js
@@ -0,0 +1,28 @@
+const React = require('react');
+
+const Icon = React.createClass({
+ propTypes: {
+ id: React.PropTypes.string,
+ type: React.PropTypes.string,
+ className: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ type: 'fa'
+ };
+ },
+
+ render() {
+ const { id, type } = this.props;
+ let { className } = this.props;
+
+ if (id) {
+ className = 'GitBook-Icon ' + type + ' ' + type + '-' + id;
+ }
+
+ return <i className={className}/>;
+ }
+});
+
+module.exports = Icon;
diff --git a/packages/gitbook-core/src/components/Import.js b/packages/gitbook-core/src/components/Import.js
new file mode 100644
index 0000000..68318b9
--- /dev/null
+++ b/packages/gitbook-core/src/components/Import.js
@@ -0,0 +1,48 @@
+const React = require('react');
+const Head = require('react-helmet');
+const ReactRedux = require('react-redux');
+
+/**
+ * Resolve a file url to a relative url in current state
+ * @param {String} href
+ * @param {State} state
+ * @return {String}
+ */
+function resolveForCurrentFile(href, state) {
+ const { file } = state;
+ return file.relative(href);
+}
+
+const ImportLink = ReactRedux.connect((state, {rel, href}) => {
+ href = resolveForCurrentFile(href, state);
+
+ return {
+ link: [
+ {
+ rel,
+ href
+ }
+ ]
+ };
+})(Head);
+
+const ImportScript = ReactRedux.connect((state, {type, src}) => {
+ src = resolveForCurrentFile(src, state);
+
+ 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
new file mode 100644
index 0000000..097edaf
--- /dev/null
+++ b/packages/gitbook-core/src/components/InjectedComponent.js
@@ -0,0 +1,117 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+const { List } = require('immutable');
+
+const { findMatchingComponents } = require('../actions/components');
+
+/*
+ Public: InjectedComponent makes it easy to include a set of dynamically registered
+ components inside of your React render method. Rather than explicitly render
+ an array of buttons, for example, you can use InjectedComponentSet:
+
+ ```js
+ <InjectedComponentSet className="message-actions"
+ matching={{role: 'ThreadActionButton'}}
+ props={{ a: 1 }}>
+ ```
+
+ InjectedComponentSet will look up components registered for the location you provide,
+ render them inside a {Flexbox} and pass them `exposedProps`. By default, all injected
+ children are rendered inside {UnsafeComponent} wrappers to prevent third-party code
+ from throwing exceptions that break React renders.
+
+ InjectedComponentSet monitors the ComponentStore for changes. If a new component
+ is registered into the location you provide, InjectedComponentSet will re-render.
+ If no matching components is found, the InjectedComponent renders an empty span.
+ */
+
+const Injection = React.createClass({
+ propTypes: {
+ component: React.PropTypes.func,
+ props: React.PropTypes.object,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ const Comp = this.props.component;
+ const { props, children } = this.props;
+
+ // TODO: try to render with an error handling for unsafe component
+ return <Comp {...props}>{children}</Comp>;
+ }
+});
+
+const InjectedComponentSet = React.createClass({
+ propTypes: {
+ components: React.PropTypes.oneOfType([
+ React.PropTypes.arrayOf(React.PropTypes.func),
+ React.PropTypes.instanceOf(List)
+ ]).isRequired,
+ props: React.PropTypes.object,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ const { components, props, children, ...divProps } = this.props;
+
+ const inner = components.map((Comp, i) => <Injection key={i} component={Comp} props={props} />);
+
+ return (
+ <div {...divProps}>
+ {children}
+ {inner}
+ </div>
+ );
+ }
+});
+
+/**
+ * Render only the first component matching
+ */
+const InjectedComponent = React.createClass({
+ propTypes: {
+ components: React.PropTypes.oneOfType([
+ React.PropTypes.arrayOf(React.PropTypes.func),
+ React.PropTypes.instanceOf(List)
+ ]).isRequired,
+ props: React.PropTypes.object,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { components, props, children } = this.props;
+
+ if (!children) {
+ children = null;
+ } else {
+ children = React.Children.only(children);
+ }
+
+ return components.reduce((inner, Comp) => {
+ return (
+ <Injection component={Comp} props={props}>
+ {inner}
+ </Injection>
+ );
+ }, children);
+ }
+});
+
+/**
+ * Map Redux state to InjectedComponentSet's props
+ */
+function mapStateToProps(state, props) {
+ const { components } = state;
+ const { matching } = props;
+
+ return {
+ components: findMatchingComponents(components, matching)
+ };
+}
+
+const connect = ReactRedux.connect(mapStateToProps);
+
+module.exports = {
+ InjectedComponent: connect(InjectedComponent),
+ InjectedComponentSet: connect(InjectedComponentSet)
+};
diff --git a/packages/gitbook-core/src/components/Link.js b/packages/gitbook-core/src/components/Link.js
new file mode 100644
index 0000000..ab364bb
--- /dev/null
+++ b/packages/gitbook-core/src/components/Link.js
@@ -0,0 +1,37 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+
+const File = require('../models/File');
+const SummaryArticle = require('../models/SummaryArticle');
+const SummaryArticleShape = require('../propTypes/SummaryArticle');
+const FileShape = require('../propTypes/File');
+
+const Link = React.createClass({
+ propTypes: {
+ currentFile: FileShape,
+ children: React.PropTypes.node,
+
+ // Destination of the link
+ to: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ SummaryArticleShape,
+ FileShape
+ ])
+ },
+
+ render() {
+ const { currentFile, to, children, ...props } = this.props;
+ let href = to;
+
+ if (SummaryArticle.is(to) || File.is(to)) {
+ href = to.url;
+ }
+
+ href = currentFile.relative(href);
+ return <a href={href} {...props}>{children}</a>;
+ }
+});
+
+module.exports = ReactRedux.connect(state => {
+ return { currentFile: state.file };
+})(Link);
diff --git a/packages/gitbook-core/src/components/PJAXWrapper.js b/packages/gitbook-core/src/components/PJAXWrapper.js
new file mode 100644
index 0000000..6ed0697
--- /dev/null
+++ b/packages/gitbook-core/src/components/PJAXWrapper.js
@@ -0,0 +1,102 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+const History = require('../actions/history');
+
+/**
+ * Check if an element is inside a link
+ * @param {DOMElement} el
+ * @param {String} name
+ * @return {DOMElement|undefined
+ */
+function findParentByTagName(el, name) {
+ while (el && el !== document) {
+ if (el.tagName && el.tagName.toUpperCase() === name) {
+ return el;
+ }
+ el = el.parentNode;
+ }
+
+ return false;
+}
+
+/**
+ * Internal: Return the `href` component of given URL object with the hash
+ * portion removed.
+ *
+ * @param {Location|HTMLAnchorElement} location
+ * @return {String}
+ */
+function stripHash(location) {
+ return location.href.replace(/#.*/, '');
+}
+
+/**
+ * Test if a click event should be handled,
+ * return the new url if it's a normal lcick
+ */
+function getHrefForEvent(event) {
+ const link = findParentByTagName(event.target, 'A');
+
+ if (!link)
+ return;
+
+ // Middle click, cmd click, and ctrl click should open
+ // links in a new tab as normal.
+ if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
+ return;
+
+ // Ignore cross origin links
+ if (location.protocol !== link.protocol || location.hostname !== link.hostname)
+ return;
+
+ // Ignore case when a hash is being tacked on the current URL
+ if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location))
+ return;
+
+ // Ignore event with default prevented
+ if (event.defaultPrevented)
+ return;
+
+ // Explicitly ignored
+ if (link.getAttribute('data-nopjax'))
+ return;
+
+ return link.pathname;
+}
+
+/*
+ Wrapper to bind all navigation events to fetch pages.
+ */
+
+const PJAXWrapper = React.createClass({
+ propTypes: {
+ children: React.PropTypes.node,
+ dispatch: React.PropTypes.func
+ },
+
+ onClick(event) {
+ const { dispatch } = this.props;
+ const href = getHrefForEvent(event);
+
+ if (!href) {
+ return;
+ }
+
+ event.preventDefault();
+ dispatch(History.push(href));
+ },
+
+ componentDidMount() {
+ document.addEventListener('click', this.onClick, false);
+ },
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.onClick, false);
+ },
+
+ render() {
+ return React.Children.only(this.props.children);
+ }
+});
+
+module.exports = ReactRedux.connect()(PJAXWrapper);
diff --git a/packages/gitbook-core/src/components/Panel.js b/packages/gitbook-core/src/components/Panel.js
new file mode 100644
index 0000000..694cc29
--- /dev/null
+++ b/packages/gitbook-core/src/components/Panel.js
@@ -0,0 +1,22 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const Panel = React.createClass({
+ propTypes: {
+ className: React.PropTypes.string,
+ children: React.PropTypes.node
+ },
+
+ render() {
+ let { className, children } = this.props;
+ className = classNames('GitBook-Panel', className);
+
+ return (
+ <div className={className}>
+ {children}
+ </div>
+ );
+ }
+});
+
+module.exports = Panel;
diff --git a/packages/gitbook-core/src/components/Tooltipped.js b/packages/gitbook-core/src/components/Tooltipped.js
new file mode 100644
index 0000000..4d297fd
--- /dev/null
+++ b/packages/gitbook-core/src/components/Tooltipped.js
@@ -0,0 +1,44 @@
+const React = require('react');
+const classNames = require('classnames');
+
+const POSITIONS = {
+ BOTTOM_RIGHT: 'e',
+ BOTTOM_LEFT: 'w',
+ TOP_LEFT: 'nw',
+ TOP_RIGHT: 'ne',
+ BOTTOM: '',
+ TOP: 'n'
+};
+
+const Tooltipped = React.createClass({
+ propTypes: {
+ title: React.PropTypes.string.isRequired,
+ position: React.PropTypes.string,
+ open: React.PropTypes.bool,
+ children: React.PropTypes.node
+ },
+
+ statics: {
+ POSITIONS
+ },
+
+ render() {
+ const { title, position, open, children } = this.props;
+
+ const className = classNames(
+ 'GitBook-Tooltipped',
+ position ? 'Tooltipped-' + position : '',
+ {
+ 'Tooltipped-o': open
+ }
+ );
+
+ return (
+ <div className={className} aria-label={title}>
+ {children}
+ </div>
+ );
+ }
+});
+
+module.exports = Tooltipped;
diff --git a/packages/gitbook-core/src/index.js b/packages/gitbook-core/src/index.js
new file mode 100644
index 0000000..3f0120c
--- /dev/null
+++ b/packages/gitbook-core/src/index.js
@@ -0,0 +1,73 @@
+require('whatwg-fetch');
+
+const React = require('react');
+const ReactCSSTransitionGroup = require('react-addons-css-transition-group');
+const Immutable = require('immutable');
+const Head = require('react-helmet');
+const Promise = require('bluebird');
+const { Provider } = require('react-redux');
+const { Flex, Box } = require('reflexbox');
+
+const { InjectedComponent, InjectedComponentSet } = require('./components/InjectedComponent');
+const { ImportLink, ImportScript, ImportCSS } = require('./components/Import');
+const HTMLContent = require('./components/HTMLContent');
+const Link = require('./components/Link');
+const Icon = require('./components/Icon');
+const HotKeys = require('./components/HotKeys');
+const Button = require('./components/Button');
+const ButtonGroup = require('./components/ButtonGroup');
+const Dropdown = require('./components/Dropdown');
+const Panel = require('./components/Panel');
+const Backdrop = require('./components/Backdrop');
+const Tooltipped = require('./components/Tooltipped');
+const I18nProvider = require('./components/I18nProvider');
+
+const ACTIONS = require('./actions/TYPES');
+
+const PropTypes = require('./propTypes');
+const connect = require('./lib/connect');
+const createPlugin = require('./lib/createPlugin');
+const createReducer = require('./lib/createReducer');
+const createContext = require('./lib/createContext');
+const composeReducer = require('./lib/composeReducer');
+const bootstrap = require('./lib/bootstrap');
+const renderWithContext = require('./lib/renderWithContext');
+
+module.exports = {
+ ACTIONS,
+ bootstrap,
+ renderWithContext,
+ connect,
+ createPlugin,
+ createReducer,
+ createContext,
+ composeReducer,
+ // React Components
+ I18nProvider,
+ InjectedComponent,
+ InjectedComponentSet,
+ HTMLContent,
+ Head,
+ Panel,
+ Provider,
+ ImportLink,
+ ImportScript,
+ ImportCSS,
+ FlexLayout: Flex,
+ FlexBox: Box,
+ Link,
+ Icon,
+ HotKeys,
+ Button,
+ ButtonGroup,
+ Dropdown,
+ Backdrop,
+ Tooltipped,
+ // Utilities
+ PropTypes,
+ // Librairies
+ React,
+ ReactCSSTransitionGroup,
+ Immutable,
+ Promise
+};
diff --git a/packages/gitbook-core/src/lib/bootstrap.js b/packages/gitbook-core/src/lib/bootstrap.js
new file mode 100644
index 0000000..f3c99b7
--- /dev/null
+++ b/packages/gitbook-core/src/lib/bootstrap.js
@@ -0,0 +1,29 @@
+const ReactDOM = require('react-dom');
+
+const getPayload = require('./getPayload');
+const createContext = require('./createContext');
+const renderWithContext = require('./renderWithContext');
+
+/**
+ * Bootstrap GitBook on the browser (this function should not be called on the server side).
+ * @param {Object} matching
+ */
+function bootstrap(matching) {
+ const initialState = getPayload(window.document);
+ const plugins = window.gitbookPlugins;
+
+ const mountNode = document.getElementById('content');
+
+ // Create the redux store
+ const context = createContext(plugins, initialState);
+
+ window.gitbookContext = context;
+
+ // Render with the store
+ const el = renderWithContext(context, matching);
+
+ ReactDOM.render(el, mountNode);
+}
+
+
+module.exports = bootstrap;
diff --git a/packages/gitbook-core/src/lib/composeReducer.js b/packages/gitbook-core/src/lib/composeReducer.js
new file mode 100644
index 0000000..fa2a589
--- /dev/null
+++ b/packages/gitbook-core/src/lib/composeReducer.js
@@ -0,0 +1,16 @@
+
+/**
+ * Compose multiple reducers into one
+ * @param {Function} reducers
+ * @return {Function}
+ */
+function composeReducer(...reducers) {
+ return (state, action) => {
+ return reducers.reduce(
+ (newState, reducer) => reducer(newState, action),
+ state
+ );
+ };
+}
+
+module.exports = composeReducer;
diff --git a/packages/gitbook-core/src/lib/connect.js b/packages/gitbook-core/src/lib/connect.js
new file mode 100644
index 0000000..a34299d
--- /dev/null
+++ b/packages/gitbook-core/src/lib/connect.js
@@ -0,0 +1,70 @@
+const React = require('react');
+const ReactRedux = require('react-redux');
+const { injectIntl } = require('react-intl');
+
+const ContextShape = require('../propTypes/Context');
+
+/**
+ * Use the GitBook context provided by ContextProvider to map actions to props
+ * @param {ReactComponent} Component
+ * @param {Function} mapActionsToProps
+ * @return {ReactComponent}
+ */
+function connectToActions(Component, mapActionsToProps) {
+ if (!mapActionsToProps) {
+ return Component;
+ }
+
+ return React.createClass({
+ displayName: `ConnectActions(${Component.displayName})`,
+ propTypes: {
+ children: React.PropTypes.node
+ },
+
+ contextTypes: {
+ gitbook: ContextShape.isRequired
+ },
+
+ render() {
+ const { gitbook } = this.context;
+ const { children, ...props } = this.props;
+ const { actions, store } = gitbook;
+
+ const actionsProps = mapActionsToProps(actions, store.dispatch);
+
+ return <Component {...props} {...actionsProps}>{children}</Component>;
+ }
+ });
+}
+
+/**
+ * Connect to i18n
+ * @param {ReactComponent} Component
+ * @return {ReactComponent}
+ */
+function connectToI18n(Component) {
+ return injectIntl(({intl, children, ...props}) => {
+ const i18n = {
+ t: (id, values) => intl.formatMessage({ id }, values)
+ };
+
+ return <Component {...props} i18n={i18n}>{children}</Component>;
+ });
+}
+
+/**
+ * Connect a component to the GitBook context (store and actions).
+ *
+ * @param {ReactComponent} Component
+ * @param {Function} mapStateToProps
+ * @return {ReactComponent}
+ */
+function connect(Component, mapStateToProps, mapActionsToProps) {
+ Component = ReactRedux.connect(mapStateToProps)(Component);
+ Component = connectToI18n(Component);
+ Component = connectToActions(Component, mapActionsToProps);
+
+ return Component;
+}
+
+module.exports = connect;
diff --git a/packages/gitbook-core/src/lib/createContext.js b/packages/gitbook-core/src/lib/createContext.js
new file mode 100644
index 0000000..ba0c7e1
--- /dev/null
+++ b/packages/gitbook-core/src/lib/createContext.js
@@ -0,0 +1,76 @@
+/* eslint-disable no-console */
+const Redux = require('redux');
+const ReduxThunk = require('redux-thunk').default;
+
+const Plugin = require('../models/Plugin');
+const Context = require('../models/Context');
+const coreReducers = require('../reducers');
+const composeReducer = require('./composeReducer');
+
+const Components = require('../actions/components');
+const I18n = require('../actions/i18n');
+const History = require('../actions/history');
+
+const isBrowser = (typeof window !== 'undefined');
+
+/**
+ * The core plugin defines the defualt behaviour of GitBook and provides
+ * actions to other plugins.
+ * @type {Plugin}
+ */
+const corePlugin = new Plugin({
+ reduce: coreReducers,
+ actions: {
+ Components, I18n, History
+ }
+});
+
+/**
+ * Create a new context containing redux store from an initial state and a list of plugins.
+ * Each plugin entry is the result of {createPlugin}.
+ *
+ * @param {Array<Plugin>} plugins
+ * @param {Object} initialState
+ * @return {Context} context
+ */
+function createContext(plugins, initialState) {
+ plugins = [corePlugin].concat(plugins);
+
+ // Compose the reducer from core with plugins
+ const pluginReducers = plugins.map(plugin => plugin.reduce);
+ const reducer = composeReducer(...pluginReducers);
+
+ // Get actions from all plugins
+ const actions = plugins.reduce((accu, plugin) => {
+ return Object.assign(accu, plugin.actions);
+ }, {});
+
+ // Create thunk middleware which include actions
+ const thunk = ReduxThunk.withExtraArgument(actions);
+
+ // Create the redux store
+ const store = Redux.createStore(
+ (state, action) => {
+ if (isBrowser) {
+ console.log('[store]', action.type);
+ }
+ return reducer(state, action);
+ },
+ initialState,
+ Redux.compose(Redux.applyMiddleware(thunk))
+ );
+
+ // Create the context
+ const context = new Context({
+ store,
+ plugins,
+ actions
+ });
+
+ // Initialize the plugins
+ context.activate();
+
+ return context;
+}
+
+module.exports = createContext;
diff --git a/packages/gitbook-core/src/lib/createPlugin.js b/packages/gitbook-core/src/lib/createPlugin.js
new file mode 100644
index 0000000..cb5d2be
--- /dev/null
+++ b/packages/gitbook-core/src/lib/createPlugin.js
@@ -0,0 +1,27 @@
+const Plugin = require('../models/Plugin');
+
+/**
+ * Create a plugin to extend the state and the views.
+ *
+ * @param {Function(dispatch, state)} plugin.init
+ * @param {Function(state, action)} plugin.reduce
+ * @param {Object} plugin.actions
+ * @return {Plugin}
+ */
+function createPlugin({ activate, deactivate, reduce, actions }) {
+ const plugin = new Plugin({
+ activate,
+ deactivate,
+ reduce,
+ actions
+ });
+
+ if (typeof window !== 'undefined') {
+ window.gitbookPlugins = window.gitbookPlugins || [];
+ window.gitbookPlugins.push(plugin);
+ }
+
+ return plugin;
+}
+
+module.exports = createPlugin;
diff --git a/packages/gitbook-core/src/lib/createReducer.js b/packages/gitbook-core/src/lib/createReducer.js
new file mode 100644
index 0000000..2ebecfb
--- /dev/null
+++ b/packages/gitbook-core/src/lib/createReducer.js
@@ -0,0 +1,27 @@
+
+/**
+ * Helper to create a reducer that extend the store.
+ *
+ * @param {String} property
+ * @param {Function(state, action): state} reduce
+ * @return {Function(state, action): state}
+ */
+function createReducer(name, reduce) {
+ return (state, action) => {
+ const value = state[name];
+ const newValue = reduce(value, action);
+
+ if (newValue === value) {
+ return state;
+ }
+
+ const newState = {
+ ...state,
+ [name]: newValue
+ };
+
+ return newState;
+ };
+}
+
+module.exports = createReducer;
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/lib/renderWithContext.js b/packages/gitbook-core/src/lib/renderWithContext.js
new file mode 100644
index 0000000..dc7e1f2
--- /dev/null
+++ b/packages/gitbook-core/src/lib/renderWithContext.js
@@ -0,0 +1,55 @@
+const React = require('react');
+
+const { InjectedComponent } = require('../components/InjectedComponent');
+const PJAXWrapper = require('../components/PJAXWrapper');
+const I18nProvider = require('../components/I18nProvider');
+const ContextProvider = require('../components/ContextProvider');
+const History = require('../actions/history');
+const contextShape = require('../propTypes/context');
+
+const GitBookApplication = React.createClass({
+ propTypes: {
+ context: contextShape,
+ matching: React.PropTypes.object
+ },
+
+ componentDidMount() {
+ const { context } = this.props;
+ context.dispatch(History.activate());
+ },
+
+ componentWillUnmount() {
+ const { context } = this.props;
+ context.dispatch(History.deactivate());
+ },
+
+ render() {
+ const { context, matching } = this.props;
+
+ return (
+ <ContextProvider context={context}>
+ <PJAXWrapper>
+ <I18nProvider>
+ <InjectedComponent matching={matching} />
+ </I18nProvider>
+ </PJAXWrapper>
+ </ContextProvider>
+ );
+ }
+});
+
+
+/**
+ * Render the application for a GitBook context.
+ *
+ * @param {GitBookContext} context
+ * @param {Object} matching
+ * @return {React.Element} element
+ */
+function renderWithContext(context, matching) {
+ return (
+ <GitBookApplication context={context} matching={matching} />
+ );
+}
+
+module.exports = renderWithContext;
diff --git a/packages/gitbook-core/src/models/Context.js b/packages/gitbook-core/src/models/Context.js
new file mode 100644
index 0000000..f4b0d4c
--- /dev/null
+++ b/packages/gitbook-core/src/models/Context.js
@@ -0,0 +1,58 @@
+const { Record, List } = require('immutable');
+
+const DEFAULTS = {
+ store: null,
+ actions: {},
+ plugins: List()
+};
+
+class Context extends Record(DEFAULTS) {
+ constructor(...args) {
+ super(...args);
+
+ this.dispatch = this.dispatch.bind(this);
+ this.getState = this.getState.bind(this);
+ }
+
+ /**
+ * Return current state
+ * @return {Object}
+ */
+ getState() {
+ const { store } = this;
+ return store.getState();
+ }
+
+ /**
+ * Dispatch an action
+ * @param {Action} action
+ */
+ dispatch(action) {
+ const { store } = this;
+ return store.dispatch(action);
+ }
+
+ /**
+ * Deactivate the context, cleanup resources from plugins.
+ */
+ deactivate() {
+ const { plugins, actions } = this;
+
+ plugins.forEach(plugin => {
+ plugin.deactivate(this.dispatch, this.getState, actions);
+ });
+ }
+
+ /**
+ * Activate the context and the plugins.
+ */
+ activate() {
+ const { plugins, actions } = this;
+
+ plugins.forEach(plugin => {
+ plugin.activate(this.dispatch, this.getState, actions);
+ });
+ }
+}
+
+module.exports = Context;
diff --git a/packages/gitbook-core/src/models/File.js b/packages/gitbook-core/src/models/File.js
new file mode 100644
index 0000000..efc4f11
--- /dev/null
+++ b/packages/gitbook-core/src/models/File.js
@@ -0,0 +1,54 @@
+const path = require('path');
+const { Record } = require('immutable');
+
+const DEFAULTS = {
+ type: '',
+ mtime: new Date(),
+ path: '',
+ url: ''
+};
+
+class File extends Record(DEFAULTS) {
+ constructor(file = {}) {
+ if (typeof file === 'string') {
+ file = { path: file, url: file };
+ }
+
+ super({
+ ...file,
+ mtime: new Date(file.mtime)
+ });
+ }
+
+ /**
+ * @param {String} to Absolute path
+ * @return {String} The same path, but relative to this file
+ */
+ relative(to) {
+ return path.relative(
+ path.dirname(this.path),
+ to
+ ) || './';
+ }
+
+ /**
+ * Return true if file is an instance of File
+ * @param {Mixed} file
+ * @return {Boolean} isFile
+ */
+ static is(file) {
+ return (file instanceof File);
+ }
+
+ /**
+ * Create a file instance
+ * @param {Mixed|File} file
+ * @return {File} file
+ */
+ static create(file) {
+ return File.is(file) ?
+ file : new File(file);
+ }
+}
+
+module.exports = File;
diff --git a/packages/gitbook-core/src/models/Language.js b/packages/gitbook-core/src/models/Language.js
new file mode 100644
index 0000000..20fc237
--- /dev/null
+++ b/packages/gitbook-core/src/models/Language.js
@@ -0,0 +1,12 @@
+const { Record } = require('immutable');
+
+const DEFAULTS = {
+ id: null,
+ title: null
+};
+
+class Language extends Record(DEFAULTS) {
+
+}
+
+module.exports = Language;
diff --git a/packages/gitbook-core/src/models/Languages.js b/packages/gitbook-core/src/models/Languages.js
new file mode 100644
index 0000000..b698d14
--- /dev/null
+++ b/packages/gitbook-core/src/models/Languages.js
@@ -0,0 +1,40 @@
+const { Record, List } = require('immutable');
+const Language = require('./Language');
+const File = require('./File');
+
+const DEFAULTS = {
+ current: String(),
+ file: new File(),
+ list: List()
+};
+
+class Languages extends Record(DEFAULTS) {
+ constructor(spec = {}) {
+ super({
+ ...spec,
+ file: File.create(spec.file),
+ list: List(spec.list).map(lang => new Language(lang))
+ });
+ }
+
+ /**
+ * Return true if file is an instance of Languages
+ * @param {Mixed} langs
+ * @return {Boolean}
+ */
+ static is(langs) {
+ return (langs instanceof Languages);
+ }
+
+ /**
+ * Create a Languages instance
+ * @param {Mixed|Languages} langs
+ * @return {Languages}
+ */
+ static create(langs) {
+ return Languages.is(langs) ?
+ langs : new Languages(langs);
+ }
+}
+
+module.exports = Languages;
diff --git a/packages/gitbook-core/src/models/Location.js b/packages/gitbook-core/src/models/Location.js
new file mode 100644
index 0000000..cdfea2d
--- /dev/null
+++ b/packages/gitbook-core/src/models/Location.js
@@ -0,0 +1,78 @@
+const { Record, Map } = require('immutable');
+const querystring = require('querystring');
+
+const DEFAULTS = {
+ pathname: String(''),
+ // Hash without the #
+ hash: String(''),
+ // If query is a non empty map
+ query: Map()
+};
+
+class Location extends Record(DEFAULTS) {
+
+ /**
+ * Return search query as a string
+ * @return {String}
+ */
+ get search() {
+ const { query } = this;
+ return query.size === 0 ?
+ '' :
+ '?' + querystring.stringify(query.toJS());
+ }
+
+ /**
+ * Convert this location to a string.
+ * @return {String}
+ */
+ toString() {
+
+ }
+
+ /**
+ * Convert this immutable instance to an object
+ * for "history".
+ * @return {Object}
+ */
+ toNative() {
+ return {
+ pathname: this.pathname,
+ hash: this.hash ? `#${this.hash}` : '',
+ search: this.search
+ };
+ }
+
+ /**
+ * Convert an instance from "history" to Location.
+ * @param {Object|String} location
+ * @return {Location}
+ */
+ static fromNative(location) {
+ if (typeof location === 'string') {
+ location = { pathname: location };
+ }
+
+ const pathname = location.pathname;
+ let hash = location.hash || '';
+ let search = location.search || '';
+ let query = location.query;
+
+ hash = hash[0] === '#' ? hash.slice(1) : hash;
+ search = search[0] === '?' ? search.slice(1) : search;
+
+ if (query) {
+ query = Map(query);
+ } else {
+ query = Map(querystring.parse(search));
+ }
+
+ return new Location({
+ pathname,
+ hash,
+ query
+ });
+ }
+}
+
+module.exports = Location;
diff --git a/packages/gitbook-core/src/models/Page.js b/packages/gitbook-core/src/models/Page.js
new file mode 100644
index 0000000..e3c4a96
--- /dev/null
+++ b/packages/gitbook-core/src/models/Page.js
@@ -0,0 +1,24 @@
+const { Record, Map, fromJS } = require('immutable');
+
+const DEFAULTS = {
+ title: '',
+ content: '',
+ dir: 'ltr',
+ depth: 1,
+ level: '',
+ previous: null,
+ next: null,
+ attributes: Map()
+};
+
+class Page extends Record(DEFAULTS) {
+ static create(state) {
+ return state instanceof Page ?
+ state : new Page({
+ ...state,
+ attributes: fromJS(state.attributes)
+ });
+ }
+}
+
+module.exports = Page;
diff --git a/packages/gitbook-core/src/models/Plugin.js b/packages/gitbook-core/src/models/Plugin.js
new file mode 100644
index 0000000..07b1976
--- /dev/null
+++ b/packages/gitbook-core/src/models/Plugin.js
@@ -0,0 +1,21 @@
+const { Record } = require('immutable');
+
+const DEFAULTS = {
+ activate: ((dispatch, getState) => {}),
+ deactivate: ((dispatch, getState) => {}),
+ reduce: ((state, action) => state),
+ actions: {}
+};
+
+class Plugin extends Record(DEFAULTS) {
+ constructor(plugin) {
+ super({
+ activate: plugin.activate || DEFAULTS.activate,
+ deactivate: plugin.deactivate || DEFAULTS.deactivate,
+ reduce: plugin.reduce || DEFAULTS.reduce,
+ actions: plugin.actions || DEFAULTS.actions
+ });
+ }
+}
+
+module.exports = Plugin;
diff --git a/packages/gitbook-core/src/models/Readme.js b/packages/gitbook-core/src/models/Readme.js
new file mode 100644
index 0000000..f275ca2
--- /dev/null
+++ b/packages/gitbook-core/src/models/Readme.js
@@ -0,0 +1,21 @@
+const { Record } = require('immutable');
+const File = require('./File');
+
+const DEFAULTS = {
+ file: new File()
+};
+
+class Readme extends Record(DEFAULTS) {
+ constructor(state = {}) {
+ super({
+ file: File.create(state.file)
+ });
+ }
+
+ static create(state) {
+ return state instanceof Readme ?
+ state : new Readme(state);
+ }
+}
+
+module.exports = Readme;
diff --git a/packages/gitbook-core/src/models/SummaryArticle.js b/packages/gitbook-core/src/models/SummaryArticle.js
new file mode 100644
index 0000000..3651c8a
--- /dev/null
+++ b/packages/gitbook-core/src/models/SummaryArticle.js
@@ -0,0 +1,32 @@
+const { Record, List } = require('immutable');
+
+const DEFAULTS = {
+ title: '',
+ depth: 0,
+ path: '',
+ url: '',
+ ref: '',
+ level: '',
+ articles: List()
+};
+
+class SummaryArticle extends Record(DEFAULTS) {
+ constructor(article) {
+ super({
+ ...article,
+ articles: (new List(article.articles))
+ .map(art => new SummaryArticle(art))
+ });
+ }
+
+ /**
+ * Return true if article is an instance of SummaryArticle
+ * @param {Mixed} article
+ * @return {Boolean}
+ */
+ static is(article) {
+ return (article instanceof SummaryArticle);
+ }
+}
+
+module.exports = SummaryArticle;
diff --git a/packages/gitbook-core/src/models/SummaryPart.js b/packages/gitbook-core/src/models/SummaryPart.js
new file mode 100644
index 0000000..89c76d4
--- /dev/null
+++ b/packages/gitbook-core/src/models/SummaryPart.js
@@ -0,0 +1,17 @@
+const { Record, List } = require('immutable');
+const SummaryArticle = require('./SummaryArticle');
+
+class SummaryPart extends Record({
+ title: '',
+ articles: List()
+}) {
+ constructor(state) {
+ super({
+ ...state,
+ articles: (new List(state.articles))
+ .map(article => new SummaryArticle(article))
+ });
+ }
+}
+
+module.exports = SummaryPart;
diff --git a/packages/gitbook-core/src/propTypes/Context.js b/packages/gitbook-core/src/propTypes/Context.js
new file mode 100644
index 0000000..dd6d010
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Context.js
@@ -0,0 +1,11 @@
+const React = require('react');
+const {
+ object,
+ shape
+} = React.PropTypes;
+
+
+module.exports = shape({
+ store: object,
+ actions: object
+});
diff --git a/packages/gitbook-core/src/propTypes/File.js b/packages/gitbook-core/src/propTypes/File.js
new file mode 100644
index 0000000..fb7bc06
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/File.js
@@ -0,0 +1,13 @@
+const React = require('react');
+const {
+ oneOf,
+ string,
+ instanceOf,
+ shape
+} = React.PropTypes;
+
+module.exports = shape({
+ mtime: instanceOf(Date).isRequired,
+ path: string.isRequired,
+ type: oneOf(['', 'markdown', 'asciidoc']).isRequired
+});
diff --git a/packages/gitbook-core/src/propTypes/History.js b/packages/gitbook-core/src/propTypes/History.js
new file mode 100644
index 0000000..1b59ea0
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/History.js
@@ -0,0 +1,11 @@
+const React = require('react');
+const locationShape = require('./Location');
+const {
+ bool,
+ shape
+} = React.PropTypes;
+
+module.exports = shape({
+ loading: bool,
+ location: locationShape
+});
diff --git a/packages/gitbook-core/src/propTypes/Language.js b/packages/gitbook-core/src/propTypes/Language.js
new file mode 100644
index 0000000..eea37a7
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Language.js
@@ -0,0 +1,7 @@
+const React = require('react');
+const { string, shape } = React.PropTypes;
+
+module.exports = shape({
+ id: string.isRequired,
+ title: string.isRequired
+});
diff --git a/packages/gitbook-core/src/propTypes/Languages.js b/packages/gitbook-core/src/propTypes/Languages.js
new file mode 100644
index 0000000..076aec5
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Languages.js
@@ -0,0 +1,12 @@
+const React = require('react');
+const { listOf } = require('react-immutable-proptypes');
+const { shape, string } = React.PropTypes;
+
+const fileShape = require('./File');
+const languageShape = require('./Language');
+
+module.exports = shape({
+ current: string.isRequired,
+ file: fileShape.isRequired,
+ list: listOf(languageShape).isRequired
+});
diff --git a/packages/gitbook-core/src/propTypes/Location.js b/packages/gitbook-core/src/propTypes/Location.js
new file mode 100644
index 0000000..13e0a34
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Location.js
@@ -0,0 +1,12 @@
+const React = require('react');
+const { map } = require('react-immutable-proptypes');
+const {
+ string,
+ shape
+} = React.PropTypes;
+
+module.exports = shape({
+ pathname: string,
+ hash: string,
+ query: map
+});
diff --git a/packages/gitbook-core/src/propTypes/Page.js b/packages/gitbook-core/src/propTypes/Page.js
new file mode 100644
index 0000000..c589f54
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Page.js
@@ -0,0 +1,16 @@
+const React = require('react');
+const {
+ oneOf,
+ string,
+ number,
+ shape
+} = React.PropTypes;
+
+
+module.exports = shape({
+ title: string.isRequired,
+ content: string.isRequired,
+ level: string.isRequired,
+ depth: number.isRequired,
+ dir: oneOf(['ltr', 'rtl']).isRequired
+});
diff --git a/packages/gitbook-core/src/propTypes/Readme.js b/packages/gitbook-core/src/propTypes/Readme.js
new file mode 100644
index 0000000..8414f05
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Readme.js
@@ -0,0 +1,11 @@
+const React = require('react');
+
+const {
+ shape
+} = React.PropTypes;
+
+const File = require('./File');
+
+module.exports = shape({
+ file: File.isRequired
+});
diff --git a/packages/gitbook-core/src/propTypes/Summary.js b/packages/gitbook-core/src/propTypes/Summary.js
new file mode 100644
index 0000000..f97e66c
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/Summary.js
@@ -0,0 +1,14 @@
+const React = require('react');
+const { listOf } = require('react-immutable-proptypes');
+
+const {
+ shape
+} = React.PropTypes;
+
+const File = require('./File');
+const Part = require('./SummaryPart');
+
+module.exports = shape({
+ file: File.isRequired,
+ parts: listOf(Part).isRequired
+});
diff --git a/packages/gitbook-core/src/propTypes/SummaryArticle.js b/packages/gitbook-core/src/propTypes/SummaryArticle.js
new file mode 100644
index 0000000..c93bdd9
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/SummaryArticle.js
@@ -0,0 +1,22 @@
+/* eslint-disable no-use-before-define */
+
+const React = require('react');
+const { list } = require('react-immutable-proptypes');
+
+const {
+ string,
+ number,
+ shape
+} = React.PropTypes;
+
+const Article = shape({
+ title: string.isRequired,
+ depth: number.isRequired,
+ path: string,
+ url: string,
+ ref: string,
+ level: string,
+ articles: list
+});
+
+module.exports = Article;
diff --git a/packages/gitbook-core/src/propTypes/SummaryPart.js b/packages/gitbook-core/src/propTypes/SummaryPart.js
new file mode 100644
index 0000000..769ddd1
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/SummaryPart.js
@@ -0,0 +1,14 @@
+const React = require('react');
+const { listOf } = require('react-immutable-proptypes');
+
+const {
+ string,
+ shape
+} = React.PropTypes;
+
+const Article = require('./SummaryArticle');
+
+module.exports = shape({
+ title: string.isRequired,
+ articles: listOf(Article)
+});
diff --git a/packages/gitbook-core/src/propTypes/i18n.js b/packages/gitbook-core/src/propTypes/i18n.js
new file mode 100644
index 0000000..372a240
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/i18n.js
@@ -0,0 +1,10 @@
+const React = require('react');
+const {
+ func,
+ shape
+} = React.PropTypes;
+
+
+module.exports = shape({
+ t: func
+});
diff --git a/packages/gitbook-core/src/propTypes/index.js b/packages/gitbook-core/src/propTypes/index.js
new file mode 100644
index 0000000..f56b78c
--- /dev/null
+++ b/packages/gitbook-core/src/propTypes/index.js
@@ -0,0 +1,19 @@
+const React = require('react');
+const ImmutablePropTypes = require('react-immutable-proptypes');
+
+module.exports = {
+ ...ImmutablePropTypes,
+ dispatch: React.PropTypes.func,
+ I18n: require('./i18n'),
+ Context: require('./Context'),
+ Page: require('./Page'),
+ File: require('./File'),
+ History: require('./History'),
+ Language: require('./Language'),
+ Languages: require('./Languages'),
+ Location: require('./Location'),
+ Readme: require('./Readme'),
+ Summary: require('./Summary'),
+ SummaryPart: require('./SummaryPart'),
+ SummaryArticle: require('./SummaryArticle')
+};
diff --git a/packages/gitbook-core/src/reducers/components.js b/packages/gitbook-core/src/reducers/components.js
new file mode 100644
index 0000000..948a3ac
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/components.js
@@ -0,0 +1,20 @@
+const { List } = require('immutable');
+const ACTION_TYPES = require('../actions/TYPES');
+
+function reduceComponents(state, action) {
+ state = state || List();
+ switch (action.type) {
+
+ case ACTION_TYPES.REGISTER_COMPONENT:
+ return state.push({
+ Component: action.Component,
+ descriptor: action.descriptor
+ });
+
+ default:
+ return state;
+
+ }
+}
+
+module.exports = reduceComponents;
diff --git a/packages/gitbook-core/src/reducers/config.js b/packages/gitbook-core/src/reducers/config.js
new file mode 100644
index 0000000..a49c602
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/config.js
@@ -0,0 +1,15 @@
+const { fromJS } = require('immutable');
+const ACTION_TYPES = require('../actions/TYPES');
+
+module.exports = (state, action) => {
+ state = fromJS(state);
+ switch (action.type) {
+
+ case ACTION_TYPES.PAGE_FETCH_END:
+ return fromJS(action.payload.config);
+
+ default:
+ return state;
+
+ }
+};
diff --git a/packages/gitbook-core/src/reducers/file.js b/packages/gitbook-core/src/reducers/file.js
new file mode 100644
index 0000000..82b0f42
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/file.js
@@ -0,0 +1,16 @@
+const ACTION_TYPES = require('../actions/TYPES');
+const File = require('../models/File');
+
+module.exports = (state, action) => {
+ state = File.create(state);
+
+ switch (action.type) {
+
+ case ACTION_TYPES.PAGE_FETCH_END:
+ return state.merge(action.payload.file);
+
+ default:
+ return state;
+
+ }
+};
diff --git a/packages/gitbook-core/src/reducers/history.js b/packages/gitbook-core/src/reducers/history.js
new file mode 100644
index 0000000..be8fe42
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/history.js
@@ -0,0 +1,82 @@
+const { Record, List } = require('immutable');
+const { createBrowserHistory, createMemoryHistory } = require('history');
+const ACTION_TYPES = require('../actions/TYPES');
+const Location = require('../models/Location');
+
+const isServerSide = (typeof window === 'undefined');
+
+const HistoryState = Record({
+ // Current location
+ location: new Location(),
+ // Are we loading a new page
+ loading: Boolean(false),
+ // Did we fail loading a page?
+ error: null,
+ // Listener for history changes
+ listeners: List(),
+ // Function to call to stop listening
+ unlisten: null,
+ // HistoryJS instance
+ client: null
+});
+
+function reduceHistory(state, action) {
+ state = state || HistoryState();
+ switch (action.type) {
+
+ case ACTION_TYPES.PAGE_FETCH_START:
+ return state.merge({
+ loading: true
+ });
+
+ case ACTION_TYPES.PAGE_FETCH_END:
+ return state.merge({
+ loading: false
+ });
+
+ case ACTION_TYPES.PAGE_FETCH_ERROR:
+ return state.merge({
+ loading: false,
+ error: action.error
+ });
+
+ case ACTION_TYPES.HISTORY_ACTIVATE:
+ const client = isServerSide ? createMemoryHistory() : createBrowserHistory();
+ const unlisten = client.listen(action.listener);
+
+ // We can't use .merge since it convert history to an immutable
+ const newState = state
+ // TODO: we should find a way to have the correct location on server side
+ .set('location', isServerSide ? new Location() : Location.fromNative(window.location))
+ .set('client', client)
+ .set('unlisten', unlisten);
+
+ return newState;
+
+ case ACTION_TYPES.HISTORY_DEACTIVATE:
+ if (state.unlisten) {
+ state.unlisten();
+ }
+
+ return state.merge({
+ client: null,
+ unlisten: null
+ });
+
+ case ACTION_TYPES.HISTORY_UPDATE:
+ return state.merge({
+ location: action.location
+ });
+
+ case ACTION_TYPES.HISTORY_LISTEN:
+ return state.merge({
+ listeners: state.listeners.push(action.listener)
+ });
+
+ default:
+ return state;
+
+ }
+}
+
+module.exports = reduceHistory;
diff --git a/packages/gitbook-core/src/reducers/i18n.js b/packages/gitbook-core/src/reducers/i18n.js
new file mode 100644
index 0000000..4ffd129
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/i18n.js
@@ -0,0 +1,27 @@
+const { Record, Map } = require('immutable');
+const ACTION_TYPES = require('../actions/TYPES');
+
+const I18nState = Record({
+ locale: 'en',
+ // Map of locale -> Map<String:String>
+ messages: Map()
+});
+
+function reduceI18n(state, action) {
+ state = state || I18nState();
+ switch (action.type) {
+
+ case ACTION_TYPES.I18N_REGISTER_LOCALE:
+ return state.merge({
+ messages: state.messages.set(action.locale,
+ state.messages.get(action.locale, Map()).merge(action.messages)
+ )
+ });
+
+ default:
+ return state;
+
+ }
+}
+
+module.exports = reduceI18n;
diff --git a/packages/gitbook-core/src/reducers/index.js b/packages/gitbook-core/src/reducers/index.js
new file mode 100644
index 0000000..a211d3b
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/index.js
@@ -0,0 +1,15 @@
+const composeReducer = require('../lib/composeReducer');
+const createReducer = require('../lib/createReducer');
+
+module.exports = composeReducer(
+ createReducer('components', require('./components')),
+ createReducer('history', require('./history')),
+ createReducer('i18n', require('./i18n')),
+ // GitBook JSON
+ createReducer('config', require('./config')),
+ createReducer('file', require('./file')),
+ createReducer('page', require('./page')),
+ createReducer('summary', require('./summary')),
+ createReducer('readme', require('./readme')),
+ createReducer('languages', require('./languages'))
+);
diff --git a/packages/gitbook-core/src/reducers/languages.js b/packages/gitbook-core/src/reducers/languages.js
new file mode 100644
index 0000000..0ec2ae4
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/languages.js
@@ -0,0 +1,12 @@
+const Languages = require('../models/Languages');
+
+module.exports = (state, action) => {
+ state = Languages.create(state);
+
+ switch (action.type) {
+
+ default:
+ return state;
+
+ }
+};
diff --git a/packages/gitbook-core/src/reducers/page.js b/packages/gitbook-core/src/reducers/page.js
new file mode 100644
index 0000000..9b94d1e
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/page.js
@@ -0,0 +1,16 @@
+const ACTION_TYPES = require('../actions/TYPES');
+const Page = require('../models/Page');
+
+module.exports = (state, action) => {
+ state = Page.create(state);
+
+ switch (action.type) {
+
+ case ACTION_TYPES.PAGE_FETCH_END:
+ return state.merge(action.payload.page);
+
+ default:
+ return state;
+
+ }
+};
diff --git a/packages/gitbook-core/src/reducers/readme.js b/packages/gitbook-core/src/reducers/readme.js
new file mode 100644
index 0000000..9e8656a
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/readme.js
@@ -0,0 +1,5 @@
+const Readme = require('../models/Readme');
+
+module.exports = (state, action) => {
+ return Readme.create(state);
+};
diff --git a/packages/gitbook-core/src/reducers/summary.js b/packages/gitbook-core/src/reducers/summary.js
new file mode 100644
index 0000000..60568ef
--- /dev/null
+++ b/packages/gitbook-core/src/reducers/summary.js
@@ -0,0 +1,28 @@
+const { Record, List } = require('immutable');
+
+const File = require('../models/File');
+const SummaryPart = require('../models/SummaryPart');
+
+
+class SummaryState extends Record({
+ file: new File(),
+ parts: List()
+}) {
+ constructor(state = {}) {
+ super({
+ ...state,
+ file: new File(state.file),
+ parts: (new List(state.parts))
+ .map(article => new SummaryPart(article))
+ });
+ }
+
+ static create(state) {
+ return state instanceof SummaryState ?
+ state : new SummaryState(state);
+ }
+}
+
+module.exports = (state, action) => {
+ return SummaryState.create(state);
+};
diff --git a/packages/gitbook-core/src/server.js b/packages/gitbook-core/src/server.js
new file mode 100644
index 0000000..0363aa0
--- /dev/null
+++ b/packages/gitbook-core/src/server.js
@@ -0,0 +1,2 @@
+const ReactDOMServer = require('react-dom/server');
+module.exports = ReactDOMServer;