diff --git a/.babelrc b/.babelrc
index 15aa9d00e..d7d00389e 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,6 @@
{
"presets": ["react", "es2015", "stage-0"],
+ "plugins": ["transform-decorators-legacy"],
"env": {
"development": {
"presets": ["react-hmre"]
diff --git a/package.json b/package.json
index e682cef5b..8cabb32fd 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"babel-core": "^6.4.5",
"babel-eslint": "^4.1.8",
"babel-loader": "^6.2.1",
+ "babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-polyfill": "^6.7.4",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
@@ -66,9 +67,13 @@
"koa-webpack-dev-middleware": "^1.2.0",
"koa-webpack-hot-middleware": "^1.0.3",
"localenv": "^0.2.2",
+ "localforage": "^1.4.2",
"lodash": "^4.13.1",
"lodash.orderby": "^4.4.0",
"marked": "^0.3.5",
+ "mobx": "^2.2.2",
+ "mobx-react": "^3.3.0",
+ "mobx-react-devtools": "^4.2.0",
"moment": "^2.13.0",
"node-dev": "^3.1.0",
"node-sass": "^3.4.2",
@@ -110,10 +115,11 @@
"webpack": "^1.12.12"
},
"devDependencies": {
+ "babel-regenerator-runtime": "^6.5.0",
+ "fsevents": "^1.0.11",
"koa-webpack-dev-middleware": "^1.2.0",
"koa-webpack-hot-middleware": "^1.0.3",
"node-dev": "^3.1.0",
- "nodemon": "^1.9.1",
- "fsevents": "^1.0.11"
+ "nodemon": "^1.9.1"
}
}
diff --git a/server/models/Document.js b/server/models/Document.js
index 778ec7c1d..28a172736 100644
--- a/server/models/Document.js
+++ b/server/models/Document.js
@@ -7,7 +7,7 @@ import {
import {
convertToMarkdown,
truncateMarkdown,
-} from '../utils/markdown';
+} from '../../src/utils/markdown';
import Atlas from './Atlas';
import Team from './Team';
import User from './User';
diff --git a/src/assets/styles/codemirror.css b/src/assets/styles/codemirror.css
index 42e5868bd..498c27006 100644
--- a/src/assets/styles/codemirror.css
+++ b/src/assets/styles/codemirror.css
@@ -309,7 +309,7 @@
}
.CodeMirror-selected { background: #d9d9d9; }
- .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+ .CodeMirror-focused .CodeMirror-selected { background: #B7D8FC; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
diff --git a/src/components/Document/Document.js b/src/components/Document/Document.js
index 6a42f1f33..d71c51ec7 100644
--- a/src/components/Document/Document.js
+++ b/src/components/Document/Document.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { observer } from 'mobx-react';
import moment from 'moment';
import marked from 'marked';
@@ -7,6 +8,17 @@ import PublishingInfo from 'components/PublishingInfo';
import styles from './Document.scss';
+const DocumentHtml = observer((props) => {
+ return (
+
+ );
+});
+
+@observer
class Document extends React.Component {
static propTypes = {
document: React.PropTypes.object.isRequired,
@@ -20,13 +32,13 @@ class Document extends React.Component {
name={ this.props.document.user.name }
timestamp={ this.props.document.createdAt }
/>
-
+
);
}
};
export default Document;
+export {
+ DocumentHtml
+};
diff --git a/src/components/Document/index.js b/src/components/Document/index.js
index afe6c39c3..a477d1b81 100644
--- a/src/components/Document/index.js
+++ b/src/components/Document/index.js
@@ -1,2 +1,6 @@
-import Document from './Document';
+import Document, { DocumentHtml } from './Document';
+
export default Document;
+export {
+ DocumentHtml,
+};
diff --git a/src/components/DropdownMenu/DropdownMenu.scss b/src/components/DropdownMenu/DropdownMenu.scss
index beda6dc53..1b927eaf8 100644
--- a/src/components/DropdownMenu/DropdownMenu.scss
+++ b/src/components/DropdownMenu/DropdownMenu.scss
@@ -16,7 +16,7 @@
.menu {
position: absolute;
- top: 42px;
+ top: $headerHeight;
right: 0;
z-index: 1000;
border: 1px solid #eee;
@@ -28,8 +28,10 @@
.menuItem {
margin: 0;
padding: 5px 10px;
+ height: 24px;
+
display: flex;
- justify-content: flex-start;
+ justify-content: space-between;
align-items: center;
cursor: pointer;
diff --git a/src/components/Layout/Layout.scss b/src/components/Layout/Layout.scss
index 40c57c913..4e53425a3 100644
--- a/src/components/Layout/Layout.scss
+++ b/src/components/Layout/Layout.scss
@@ -23,7 +23,7 @@
align-items: center;
padding: 0 20px;
- height: 42px;
+ height: $headerHeight;
border-bottom: 1px solid #eee;
font-size: 14px;
diff --git a/src/components/MarkdownEditor/MarkdownEditor.js b/src/components/MarkdownEditor/MarkdownEditor.js
index de222b591..fbbb9da85 100644
--- a/src/components/MarkdownEditor/MarkdownEditor.js
+++ b/src/components/MarkdownEditor/MarkdownEditor.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { observer } from 'mobx-react';
import Codemirror from 'react-codemirror';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/javascript/javascript';
@@ -13,6 +14,7 @@ import './codemirror.scss';
import { client } from '../../utils/ApiClient';
+@observer
class MarkdownAtlas extends React.Component {
static propTypes = {
text: React.PropTypes.string,
@@ -111,6 +113,7 @@ class MarkdownAtlas extends React.Component {
matchBrackets: true,
lineWrapping: true,
viewportMargin: Infinity,
+ scrollbarStyle: 'null',
theme: 'atlas',
extraKeys: {
Enter: 'newlineAndIndentContinueMarkdownList',
diff --git a/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss b/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss
index f4a03faea..7d9c74b47 100644
--- a/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss
+++ b/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss
@@ -1,5 +1,5 @@
.container {
- padding-top: 100px;
+ padding-top: 50px;
cursor: text;
}
diff --git a/src/components/Preview/Preview.js b/src/components/Preview/Preview.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/components/Preview/index.js b/src/components/Preview/index.js
deleted file mode 100644
index 1f98dc6f9..000000000
--- a/src/components/Preview/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import Preview from './Preview';
-export default Preview;
diff --git a/src/components/Switch.js b/src/components/Switch.js
new file mode 100644
index 000000000..936bd1fca
--- /dev/null
+++ b/src/components/Switch.js
@@ -0,0 +1,69 @@
+import React from 'react'
+import { Base } from 'rebass'
+
+/**
+ * Binary toggle switch component
+ */
+
+const Switch = ({
+ checked,
+ ...props
+}) => {
+ const scale = '18';
+ const colors = {
+ success: '#2196F3',
+ white: '#fff',
+ };
+ const borderColor = '#2196F3';
+
+
+ const color = checked ? colors.success : borderColor
+ const transform = checked ? `translateX(${scale * 0.5}px)` : 'translateX(0)'
+
+ const sx = {
+ root: {
+ display: 'inline-flex',
+ width: scale * 1.5,
+ height: scale,
+ color,
+ backgroundColor: checked ? 'currentcolor' : null,
+ borderRadius: 99999,
+ boxShadow: 'inset 0 0 0 2px',
+ cursor: 'pointer'
+ },
+ dot: {
+ width: scale,
+ height: scale,
+ transitionProperty: 'transform, color',
+ transitionDuration: '.1s',
+ transitionTimingFunction: 'ease-out',
+ transform,
+ boxShadow: 'inset 0 0 0 2px',
+ borderRadius: 99999,
+ color,
+ backgroundColor: colors.white
+ }
+ }
+
+ return (
+
+
+
+ )
+}
+
+Switch.propTypes = {
+ /** Sets the Switch to an active style */
+ checked: React.PropTypes.bool
+}
+
+Switch.contextTypes = {
+ rebass: React.PropTypes.object
+}
+
+export default Switch
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 7629245f5..83e5d3fab 100644
--- a/src/index.js
+++ b/src/index.js
@@ -10,6 +10,7 @@ import { persistStore, autoRehydrate } from 'redux-persist';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import History from 'utils/History';
+import DevTools from 'mobx-react-devtools';
import auth from 'utils/auth';
@@ -56,21 +57,24 @@ persistStore(store, {
]
}, () => {
render((
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+ { __DEV__ ?
: null }
+
), document.getElementById('root'));
});
diff --git a/src/reducers/editor.js b/src/reducers/editor.js
deleted file mode 100644
index 2c1075097..000000000
--- a/src/reducers/editor.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const initialState = {
- originalText: null,
- text: null,
- title: null,
- unsavedChanges: false,
-};
-
-const parseHeader = (text) => {
- const firstLine = text.split(/\r?\n/)[0];
- const match = firstLine.match(/^#+ +(.*)$/);
-
- if (match) {
- return match[1];
- }
-}
-
-const editor = (state = initialState, action) => {
- switch (action.type) {
- case 'EDITOR_RESET': {
- return {
- ...initialState,
- }
- }
- case 'EDITOR_UPDATE_TITLE': {
- return {
- ...state,
- title: action.payload,
- }
- }
- case 'EDITOR_UPDATE_TEXT': {
- const title = parseHeader(action.payload);
-
- console.log(title);
-
- let unsavedChanges = false;
- if (state.originalText !== action.payload) {
- unsavedChanges = true;
- }
- return {
- ...state,
- unsavedChanges,
- text: action.payload,
- title: title || state.title,
- };
- }
- case 'EDITOR_REPLACE_TEXT': {
- const newText = state.text.replace(
- action.payload.original,
- action.payload.new
- );
-
- return {
- ...state,
- unsavedChanges: true,
- text: newText,
- };
- }
- default:
- return state;
- }
-};
-
-export default editor;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index ad7b94f3b..0fb0024ba 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -3,13 +3,11 @@ import { combineReducers } from 'redux';
import atlases from './atlases';
import document from './document';
import team from './team';
-import editor from './editor';
import user from './user';
export default combineReducers({
atlases,
document,
team,
- editor,
user,
});
diff --git a/src/scenes/DocumentEdit/DocumentEdit.js b/src/scenes/DocumentEdit/DocumentEdit.js
index d968ca34b..e4fecf21e 100644
--- a/src/scenes/DocumentEdit/DocumentEdit.js
+++ b/src/scenes/DocumentEdit/DocumentEdit.js
@@ -1,73 +1,45 @@
import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
+import { observer } from 'mobx-react';
-import {
- resetEditor,
- updateText,
- updateTitle,
- replaceText,
-} from 'actions/EditorActions';
-import {
- resetDocument,
- fetchDocumentAsync,
- saveDocumentAsync,
-} from 'actions/DocumentActions';
+import state from './DocumentEditState';
-import Layout, { Title } from 'components/Layout';
+import Switch from 'components/Switch';
+import Layout, { Title, HeaderAction } from 'components/Layout';
import Flex from 'components/Flex';
import MarkdownEditor from 'components/MarkdownEditor';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
+import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import SaveAction from './components/SaveAction';
+import Preview from './components/Preview';
+import EditorPane from './components/EditorPane';
+import styles from './DocumentEdit.scss';
+import classNames from 'classnames/bind';
+const cx = classNames.bind(styles);
+
+@observer
class DocumentEdit extends Component {
- static propTypes = {
- updateText: React.PropTypes.func.isRequired,
- updateTitle: React.PropTypes.func.isRequired,
- replaceText: React.PropTypes.func.isRequired,
- resetDocument: React.PropTypes.func.isRequired,
- saveDocumentAsync: React.PropTypes.func.isRequired,
- text: React.PropTypes.string,
- title: React.PropTypes.string,
- isSaving: React.PropTypes.bool,
- }
-
- state = {
- loadingDocument: false,
- }
-
- componentWillMount = () => {
- this.props.resetEditor();
- this.props.resetDocument();
- }
-
componentDidMount = () => {
- const id = this.props.routeParams.id;
- this.props.fetchDocumentAsync(id);
- }
-
- componentWillReceiveProps = (nextProps) => {
- if (!this.props.document && nextProps.document) {
- const doc = nextProps.document;
- this.props.updateText(doc.text);
- this.props.updateTitle(doc.title);
- }
+ state.documentId = this.props.params.id;
+ state.fetchDocument();
}
onSave = () => {
- if (this.props.title.length === 0) {
- alert("Please add a title before saving (hint: Write a markdown header)");
- return
- }
+ // if (this.props.title.length === 0) {
+ // alert("Please add a title before saving (hint: Write a markdown header)");
+ // return
+ // }
+ state.updateDocument();
+ }
- this.props.saveDocumentAsync(
- null,
- this.props.document.id,
- this.props.title,
- this.props.text,
- )
+ state = {}
+
+ onScroll = (scrollTop) => {
+ this.setState({
+ scrollTop: scrollTop,
+ })
}
render() {
@@ -76,62 +48,60 @@ class DocumentEdit extends Component {
truncate={ 60 }
placeholder={ "Untitle document" }
>
- { this.props.title }
+ { state.title }
);
+ const actions = (
+
+
+
+
+
+
+
+
+ );
return (
-
-
- )}
+ actions={ actions }
title={ title }
fixed={ true }
- loading={ this.props.isSaving }
+ loading={ state.isSaving }
>
- { (this.props.isLoading && !this.props.document) ? (
+ { (state.isFetching) ? (
) : (
-
+
+
+
+
+ { state.preview ? (
+
+
+
+ ) : null }
+
) }
);
}
}
-const mapStateToProps = (state) => {
- return {
- document: state.document.data,
- text: state.editor.text,
- title: state.editor.title,
- isLoading: state.document.isLoading,
- isSaving: state.document.isSaving,
- };
-};
-
-const mapDispatchToProps = (dispatch) => {
- return bindActionCreators({
- resetEditor,
- updateText,
- updateTitle,
- replaceText,
- resetDocument,
- fetchDocumentAsync,
- saveDocumentAsync,
- }, dispatch)
-};
-
-DocumentEdit = connect(
- mapStateToProps,
- mapDispatchToProps,
-)(DocumentEdit);
-
export default DocumentEdit;
diff --git a/src/scenes/DocumentEdit/DocumentEdit.scss b/src/scenes/DocumentEdit/DocumentEdit.scss
new file mode 100644
index 000000000..030f34a86
--- /dev/null
+++ b/src/scenes/DocumentEdit/DocumentEdit.scss
@@ -0,0 +1,45 @@
+@import '../../utils/constants.scss';
+
+.preview {
+ display: flex;
+ flex: 1;
+ padding: 50px 0;
+ padding: 50px 3em;
+ max-width: 50em;
+ line-height: 1.5em;
+}
+
+.container {
+ display: flex;
+ position: fixed;
+ top: $headerHeight;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+.editorPane {
+ flex: 0 0 50%;
+ justify-content: center;
+ overflow: scroll;
+}
+
+.paneContent {
+ flex: 1;
+ justify-content: center;
+}
+
+.fullWidth {
+ flex: 1;
+ display: flex;
+
+ .paneContent {
+ display: flex;
+ }
+}
+
+:global {
+ ::-webkit-scrollbar {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/src/scenes/DocumentEdit/DocumentEditState.js b/src/scenes/DocumentEdit/DocumentEditState.js
new file mode 100644
index 000000000..2accd4752
--- /dev/null
+++ b/src/scenes/DocumentEdit/DocumentEditState.js
@@ -0,0 +1,108 @@
+import { observable, action, computed, autorun } from 'mobx';
+import { client } from 'utils/ApiClient';
+import localforage from 'localforage';
+import { convertToMarkdown } from 'utils/markdown';
+import { browserHistory } from 'react-router'
+
+const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS';
+
+const parseHeader = (text) => {
+ const firstLine = text.split(/\r?\n/)[0];
+ const match = firstLine.match(/^#+ +(.*)$/);
+
+ if (match) {
+ return match[1];
+ }
+}
+
+const documentEditState = new class DocumentEditState {
+ @observable documentId = null;
+ @observable title = 'title';
+ @observable text = 'default state';
+
+ @observable preview;
+ @observable isFetching;
+ @observable isSaving;
+
+ /* Computed */
+
+ @computed get htmlPreview() {
+ // Only compute if preview is active
+ // if (this.preview) {
+
+ // }
+ return convertToMarkdown(this.text);
+ }
+
+ /* Actions */
+
+ @action fetchDocument = async () => {
+ this.isFetching = true;
+
+ try {
+ const data = await client.post('/documents.info', {
+ id: this.documentId,
+ })
+ const { title, text } = data.data;
+ this.title = title;
+ this.text = text;
+ } catch (e) {
+ console.error("Something went wrong");
+ }
+ this.isFetching = false;
+ }
+
+ @action updateDocument = async (nextPath) => {
+ if (this.isSaving) return;
+
+ this.isSaving = true;
+
+ try {
+ const data = await client.post('/documents.update', {
+ id: this.documentId,
+ title: this.title,
+ text: this.text,
+ })
+ browserHistory.push(`/atlas/${data.data.atlas.id}`);
+ } catch (e) {
+ console.error("Something went wrong");
+ }
+ this.isSaving = false;
+ }
+
+ @action updateText = (text) => {
+ this.text = text;
+ this.title = parseHeader(text);
+ }
+
+ @action updateTitle = (title) => {
+ this.title = title;
+ }
+
+ @action replaceText = (args) => {
+ this.text = this.text.replace(args.original, args.new);
+ }
+
+ @action togglePreview = () => {
+ console.log('toggle')
+ this.preview = !this.preview;
+ }
+
+ constructor() {
+ // Rehydrate
+ localforage.getItem(DOCUMENT_EDIT_SETTINGS)
+ .then(data => {
+ this.preview = data.preview;
+ });
+ }
+}();
+
+// Persist settings to localStorage
+autorun(() => {
+ localforage.setItem(DOCUMENT_EDIT_SETTINGS, {
+ preview: documentEditState.preview,
+ });
+});
+
+
+export default documentEditState;
\ No newline at end of file
diff --git a/src/scenes/DocumentEdit/components/EditorPane.js b/src/scenes/DocumentEdit/components/EditorPane.js
new file mode 100644
index 000000000..fcbb0fc3a
--- /dev/null
+++ b/src/scenes/DocumentEdit/components/EditorPane.js
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import styles from '../DocumentEdit.scss';
+import classNames from 'classnames/bind';
+const cx = classNames.bind(styles);
+
+class EditorPane extends React.Component {
+ static propTypes = {
+ children: React.PropTypes.node.isRequired,
+ onScroll: React.PropTypes.func.isRequired,
+ scrollTop: React.PropTypes.number,
+ fullWidth: React.PropTypes.bool,
+ }
+
+ componentWillReceiveProps = (nextProps) => {
+
+ if (nextProps.scrollTop) {
+ this.scrollToPosition(nextProps.scrollTop)
+ }
+ }
+
+ componentDidMount = () => {
+ this.refs.pane.addEventListener('scroll', this.handleScroll);
+ }
+
+ componentWillUnmount = () => {
+ this.refs.pane.removeEventListener('scroll', this.handleScroll);
+ }
+
+ handleScroll = (e) => {
+ setTimeout(() => {
+ const element = this.refs.pane;
+ const contentEl = this.refs.content;
+ this.props.onScroll(element.scrollTop / contentEl.offsetHeight);
+ }, 50);
+ }
+
+ scrollToPosition = (percentage) => {
+ const contentEl = this.refs.content;
+
+ // Push to edges
+ if (percentage < 0.02) percentage = 0;
+ if (percentage > 0.99) percentage = 100;
+
+ this.refs.pane.scrollTop = percentage * contentEl.offsetHeight;
+ }
+
+ render() {
+ return (
+
+
+ { this.props.children }
+
+
+ );
+ }
+};
+
+export default EditorPane;
diff --git a/src/scenes/DocumentEdit/components/Preview.js b/src/scenes/DocumentEdit/components/Preview.js
new file mode 100644
index 000000000..46615fa84
--- /dev/null
+++ b/src/scenes/DocumentEdit/components/Preview.js
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { DocumentHtml } from 'components/Document';
+
+import styles from '../DocumentEdit.scss';
+import classNames from 'classnames/bind';
+const cx = classNames.bind(styles);
+
+const Preview = (props) => {
+ return (
+
+
+
+ );
+};
+
+Preview.propTypes = {
+ html: React.PropTypes.string.isRequired,
+};
+
+export default Preview;
\ No newline at end of file
diff --git a/src/scenes/DocumentEdit/components/SaveAction.js b/src/scenes/DocumentEdit/components/SaveAction.js
index cba36ddbc..948cb59e0 100644
--- a/src/scenes/DocumentEdit/components/SaveAction.js
+++ b/src/scenes/DocumentEdit/components/SaveAction.js
@@ -1,12 +1,16 @@
import React from 'react';
-import { Arrow } from 'rebass';
+import { observer } from 'mobx-react';
+@observer
class SaveAction extends React.Component {
propTypes = {
onClick: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
}
onClick = (event) => {
+ if (this.props.disabled) return;
+
event.preventDefault();
this.props.onClick();
}
@@ -14,7 +18,11 @@ class SaveAction extends React.Component {
render() {
return (
);
}
diff --git a/src/stores/UiStateStore.js b/src/stores/UiStateStore.js
new file mode 100644
index 000000000..481ae8f7f
--- /dev/null
+++ b/src/stores/UiStateStore.js
@@ -0,0 +1,8 @@
+import { observable, action } from 'mobx';
+
+class UiState {
+
+}
+
+const singleton = new UiState();
+export default singleton;
\ No newline at end of file
diff --git a/src/utils/Markdown.js b/src/utils/MarkdownOld.js
similarity index 100%
rename from src/utils/Markdown.js
rename to src/utils/MarkdownOld.js
diff --git a/src/utils/constants.scss b/src/utils/constants.scss
index 816934655..3445e1db6 100644
--- a/src/utils/constants.scss
+++ b/src/utils/constants.scss
@@ -1,6 +1,8 @@
$textColor: #171B35;
$linkColor: #0C77F8;
+$headerHeight: 42px;
+
:export {
textColor: $textColor;
}
\ No newline at end of file
diff --git a/server/utils/markdown.js b/src/utils/markdown.js
similarity index 100%
rename from server/utils/markdown.js
rename to src/utils/markdown.js
diff --git a/src/utils/markdown2.js b/src/utils/markdown2.js
new file mode 100644
index 000000000..c53554562
--- /dev/null
+++ b/src/utils/markdown2.js
@@ -0,0 +1,58 @@
+import slug from 'slug';
+import truncate from 'truncate-html';
+import marked, { Renderer } from 'marked';
+import highlight from 'highlight.js';
+
+slug.defaults.mode ='rfc3986';
+
+const renderer = new Renderer();
+renderer.code = (code, language) => {
+ const validLang = !!(language && highlight.getLanguage(language));
+ const highlighted = validLang ? highlight.highlight(language, code).value : code;
+ return `${highlighted}
`;
+};
+renderer.heading = (text, level) => {
+ const headingSlug = slug(text);
+ return `
+
+
+
+
+ ${text}
+
+ `;
+},
+
+marked.setOptions({
+ renderer: renderer,
+ gfm: true,
+ tables: true,
+ breaks: false,
+ pedantic: false,
+ sanitize: true,
+ smartLists: true,
+ smartypants: true,
+});
+
+// TODO: This is syncronous and can be costly,
+// should be performed outside http request
+const convertToMarkdown = (text) => {
+ return marked(text);
+};
+
+truncate.defaultOptions = {
+ stripTags: false,
+ ellipsis: '...',
+ decodeEntities: false,
+ excludes: ['h1', 'pre', ],
+};
+
+const truncateMarkdown = (text, length) => {
+ const html = convertToMarkdown(text);
+ return truncate(html, length);
+};
+
+export {
+ convertToMarkdown,
+ truncateMarkdown,
+};
diff --git a/webpack.config.dev.js b/webpack.config.dev.js
index 154c4d4ba..a83d608f1 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.js
@@ -8,6 +8,7 @@ const developmentWebpackConfig = Object.assign(commonWebpackConfig, {
cache: true,
devtool: 'eval',
entry: [
+ 'babel-regenerator-runtime',
'webpack-hot-middleware/client',
'./src/index',
],