Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e1a0c0f422 | ||
|
7fdaf8e3ab | ||
|
c633afa184 | ||
|
368d19dd21 | ||
|
8c5ac6ade7 | ||
|
70a22ad5ff | ||
|
bd7161a727 | ||
|
f521e78bd2 | ||
|
ed7ad72c2a | ||
|
cc515695a8 | ||
|
136e9c1c8e | ||
|
a2c2407d3c | ||
|
ae5d02b288 | ||
|
966bdae3e1 | ||
|
f15069f42d | ||
|
2d5d72772d | ||
|
5a55902b5b | ||
|
dbc4bac6a2 | ||
|
30609b17da | ||
|
ab1229e8da | ||
|
3326eaccb7 | ||
|
c764eb5b45 | ||
|
be5c011349 | ||
|
aa8f43c733 | ||
|
c49cf68c74 | ||
|
d2fdf8b170 | ||
|
26f3a4d97f | ||
|
0515da6468 | ||
|
f81f54a610 | ||
|
573b6b32e2 | ||
|
b54dcf0c2c | ||
|
638ec426e5 | ||
|
7bc937606d | ||
|
ba4c8820ba | ||
|
4fe73b91bc | ||
|
9dc35f2079 | ||
|
73f0b5f4b3 | ||
|
24167d5b45 | ||
|
de45c9b411 | ||
|
9a7dc3c138 | ||
|
3bf6223c92 | ||
|
f27d91a803 | ||
|
697f105a27 | ||
|
98e5b186f1 | ||
|
e2114ae55c | ||
|
b63474b3ae | ||
|
3aa913e8c0 | ||
|
b49041db43 | ||
|
92fceed3d6 | ||
|
a7d7f551a7 | ||
|
68cdbe4cc7 | ||
|
ee54bae9d9 | ||
|
4470b9caef | ||
|
97d5f59c3f | ||
|
4562152a20 | ||
|
b414f9f181 | ||
|
2a4fdeeb96 | ||
|
ee3bd40ef7 | ||
|
4917885686 | ||
|
43ea540556 | ||
|
e84561898f | ||
|
515960a3c5 | ||
|
b385c233b6 | ||
|
cb85cb7875 | ||
|
726af737bd | ||
|
a3e267cf78 | ||
|
b554387817 | ||
|
276d034468 | ||
|
11828c2d2e | ||
|
59861bd039 | ||
|
2d191fbc00 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Adam Brown
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
13
README.md
13
README.md
@ -39,10 +39,10 @@ To build and launch a dev server:
|
|||||||
npm start
|
npm start
|
||||||
npm run server
|
npm run server
|
||||||
|
|
||||||
To build and launch the distribution server:
|
To build and launch the production server:
|
||||||
|
|
||||||
npm run build:dist
|
npm run build:prod
|
||||||
npm run server:dist
|
npm run serve:prod
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -62,8 +62,13 @@ On a low-memory machine, eg. a DigitalOcean 512MB instance, you will need to ena
|
|||||||
|
|
||||||
To make the application start on boot, run the following:
|
To make the application start on boot, run the following:
|
||||||
|
|
||||||
pm2 start grunt --name dubdiff -- serve:dist
|
# initialize pm2 to start on boot with the systemd boot manager
|
||||||
pm2 startup systemd
|
pm2 startup systemd
|
||||||
|
|
||||||
|
# start the app with pm2
|
||||||
|
pm2 start npm --name dubdiff -- run serve:prod
|
||||||
|
|
||||||
|
# save the current pm2 config so that it can be reloaded on boot
|
||||||
pm2 save
|
pm2 save
|
||||||
|
|
||||||
[Digital Ocean: How To Set Up a Node.js Application for Production on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04)
|
[Digital Ocean: How To Set Up a Node.js Application for Production on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04)
|
34
TODO.md
34
TODO.md
@ -1,33 +1 @@
|
|||||||
|
Support for displaying and responding to `#markdown` path suffix.
|
||||||
- create production mode build and serve settings: `webpack.js` and `src/server/babel.index.js`
|
|
||||||
|
|
||||||
|
|
||||||
## State changes
|
|
||||||
|
|
||||||
main page edits input documents
|
|
||||||
compare page views compare documents
|
|
||||||
|
|
||||||
compare button:
|
|
||||||
|
|
||||||
- generate id
|
|
||||||
- post input documents to server
|
|
||||||
- input documents copied to compare documents
|
|
||||||
- input documents cleared
|
|
||||||
- go to compare route
|
|
||||||
|
|
||||||
edit button:
|
|
||||||
|
|
||||||
- compare documents copied to input documents
|
|
||||||
- compare documents cleared
|
|
||||||
- go to main route
|
|
||||||
|
|
||||||
client start:
|
|
||||||
|
|
||||||
- load input documents from localStore
|
|
||||||
|
|
||||||
server start:
|
|
||||||
|
|
||||||
- load compare documents from database
|
|
||||||
|
|
||||||
|
|
||||||
* client actually never needs to query server for compare documents... huh!
|
|
8
dist/main.css
vendored
8
dist/main.css
vendored
@ -1,3 +1,11 @@
|
|||||||
#masthead .header {
|
#masthead .header {
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ins {
|
||||||
|
background-color: #dbffdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
background-color: #ffdddd;
|
||||||
|
}
|
||||||
|
10
package.json
10
package.json
@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "dubdiff-2",
|
"name": "dubdiff",
|
||||||
"version": "0.0.0",
|
"version": "2.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/server/babel.index.js",
|
"main": "src/server/babel.index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"copy-css": "cpy --parents --cwd=./node_modules/semantic-ui-css semantic.min.css themes/default/assets/fonts/icons.woff2 ../../dist",
|
"copy-css": "cpy --parents --cwd=./node_modules/semantic-ui-css semantic.min.css themes/default/assets/fonts/icons.woff2 ../../dist",
|
||||||
"build": "npm run copy-css && webpack --progress --colors",
|
"build": "npm run copy-css && webpack --progress --colors",
|
||||||
"build:prod": "npm run copy-css && cross-env NODE_ENV=production webpack -p --progress --colors",
|
"build:prod": "npm run copy-css && cross-env NODE_ENV=production webpack -p --progress --colors",
|
||||||
|
"build:prod:nocopy": "cross-env NODE_ENV=production webpack -p --progress --colors",
|
||||||
"build:watch": "npm run copy-css && webpack --progress --colors --watch",
|
"build:watch": "npm run copy-css && webpack --progress --colors --watch",
|
||||||
"serve": "node src/server/babel.index.js",
|
"serve": "node src/server/babel.index.js",
|
||||||
"serve:prod": "cross-env NODE_ENV=production node src/server/babel.index.js",
|
"serve:prod": "cross-env NODE_ENV=production node src/server/babel.index.js",
|
||||||
"webpack-stats": "webpack --json > stats.json",
|
"webpack-stats": "webpack --json > stats.json",
|
||||||
|
"lint": "standard --verbose | snazzy",
|
||||||
|
"lint:fix": "standard --fix --verbose | snazzy",
|
||||||
"test": "mocha --watch --compilers js:babel-register"
|
"test": "mocha --watch --compilers js:babel-register"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -49,10 +52,13 @@
|
|||||||
"babel-register": "^6.18.0",
|
"babel-register": "^6.18.0",
|
||||||
"chai": "^3.5.0",
|
"chai": "^3.5.0",
|
||||||
"copyfiles": "^0.2.2",
|
"copyfiles": "^0.2.2",
|
||||||
|
"cpy-cli": "^1.0.1",
|
||||||
"cross-env": "^3.1.3",
|
"cross-env": "^3.1.3",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"mocha": "^3.2.0",
|
"mocha": "^3.2.0",
|
||||||
"piping": "^1.0.0-rc.4",
|
"piping": "^1.0.0-rc.4",
|
||||||
|
"snazzy": "^6.0.0",
|
||||||
|
"standard": "^8.6.0",
|
||||||
"webpack": "^2.1.0-beta.27"
|
"webpack": "^2.1.0-beta.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,75 +1,71 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
|
|
||||||
import * as Actions from '../common/actions'
|
import * as Actions from '../common/actions'
|
||||||
import * as Selectors from '../common/selectors'
|
|
||||||
|
|
||||||
|
|
||||||
/* This component reads the local storage store and adds them to the Redux store.
|
/* This component reads the local storage store and adds them to the Redux store.
|
||||||
* Local storage is read during the componentDidMount lifecycle method.
|
* Local storage is read during the componentDidMount lifecycle method.
|
||||||
* Local storage is written during the componentWillReceiveProps lifecycle method.
|
* Local storage is written during the componentWillReceiveProps lifecycle method.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//an app-specific name for the localStorage state
|
// an app-specific name for the localStorage state
|
||||||
const stateName = "dubdiff_state"
|
const stateName = 'dubdiff_state'
|
||||||
|
|
||||||
//return a new object with the given keys, each assigned to the cooresponding value
|
// return a new object with the given keys, each assigned to the cooresponding value
|
||||||
//from the given object
|
// from the given object
|
||||||
const copyKeys = (obj, keys) => keys.reduce((acc, p)=>{acc[p]=obj[p]; return acc}, {})
|
const copyKeys = (obj, keys) => keys.reduce((acc, p) => { acc[p] = obj[p]; return acc }, {})
|
||||||
|
|
||||||
//utility method for retrieving json data from the local store
|
// utility method for retrieving json data from the local store
|
||||||
|
/*
|
||||||
function getLocalState (keys) {
|
function getLocalState (keys) {
|
||||||
if (localStorage.getItem(stateName)) {
|
if (window.localStorage.getItem(stateName)) {
|
||||||
const localState = JSON.parse(localStorage.getItem(stateName))
|
const localState = JSON.parse(window.localStorage.getItem(stateName))
|
||||||
return copyKeys(localState, keys)
|
return copyKeys(localState, keys)
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
return copyKeys({}, keys)
|
return copyKeys({}, keys)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
//utility method for writing json data to the local store
|
// utility method for writing json data to the local store
|
||||||
function setLocalState (state, keys) {
|
function setLocalState (state, keys) {
|
||||||
let toSave = copyKeys(state, keys)
|
let toSave = copyKeys(state, keys)
|
||||||
localStorage.setItem(stateName, JSON.stringify(toSave))
|
window.localStorage.setItem(stateName, JSON.stringify(toSave))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
input: state.input,
|
input: state.input
|
||||||
//the loading/empty/clean state
|
// the loading/empty/clean state
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
|
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
|
||||||
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text)),
|
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class LocalStorage extends React.Component {
|
class LocalStorage extends React.Component {
|
||||||
|
|
||||||
//load the state from the local storage
|
// load the state from the local storage
|
||||||
componentDidMount() {
|
componentDidMount () {
|
||||||
//only if the status is EMPTY
|
// only if the status is EMPTY
|
||||||
/*
|
/*
|
||||||
if (this.props.input.original=='' && this.props.input.final == '') {
|
if (this.props.input.original=='' && this.props.input.final == '') {
|
||||||
const localState = getLocalState(['input'])
|
const localState = getLocalState(['input'])
|
||||||
if (localState.input && localState.input.original)
|
if (localState.input && localState.input.original)
|
||||||
this.props.onChangeOriginal(localState.input.original)
|
this.props.onChangeOriginal(localState.input.original)
|
||||||
if (localState.input && localState.input.final)
|
if (localState.input && localState.input.final)
|
||||||
this.props.onChangeFinal(localState.input.final)
|
this.props.onChangeFinal(localState.input.final)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
//save the state to local storage
|
// save the state to local storage
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
setLocalState(nextProps, ['input'])
|
setLocalState(nextProps, ['input'])
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return this.props.children
|
return this.props.children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(LocalStorage)
|
export default connect(mapStateToProps, mapDispatchToProps)(LocalStorage)
|
||||||
|
@ -5,26 +5,25 @@ import * as Redux from 'redux'
|
|||||||
|
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
|
|
||||||
//import createBrowserHistory from 'history/lib/createBrowserHistory'
|
// import createBrowserHistory from 'history/lib/createBrowserHistory'
|
||||||
import {Router, Route, IndexRoute, Redirect, browserHistory } from 'react-router'
|
import {Router, browserHistory} from 'react-router'
|
||||||
|
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
|
|
||||||
import * as reducers from '../common/reducers'
|
import * as reducers from '../common/reducers'
|
||||||
import routes from '../common/routes'
|
import routes from '../common/routes'
|
||||||
import * as Actions from '../common/actions'
|
import {Format} from '../common/constants'
|
||||||
|
import * as Actions from '../common/actions'
|
||||||
|
|
||||||
import LocalStorage from './LocalStorage'
|
import LocalStorage from './LocalStorage'
|
||||||
|
|
||||||
|
// initial state is rehydrated from the server
|
||||||
|
const initialState = JSON.parse(decodeURI(window.__INITIAL_STATE__))
|
||||||
|
|
||||||
|
// create the redux store
|
||||||
//initial state is rehydrated from the server
|
// initial state is retrieved from localStore
|
||||||
const initialState = window.__INITIAL_STATE__
|
|
||||||
|
|
||||||
//create the redux store
|
|
||||||
//initial state is retrieved from localStore
|
|
||||||
const store = Redux.createStore(
|
const store = Redux.createStore(
|
||||||
Redux.combineReducers(reducers),
|
Redux.combineReducers(reducers),
|
||||||
initialState,
|
initialState,
|
||||||
Redux.compose(
|
Redux.compose(
|
||||||
Redux.applyMiddleware(thunk),
|
Redux.applyMiddleware(thunk),
|
||||||
@ -32,19 +31,58 @@ const store = Redux.createStore(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
||||||
|
// read and init the hash after render:
|
||||||
|
// because this parameter is passed as a hash, it isn't available on the server,
|
||||||
|
// so the server initial render will be with plaintext, so we have to start with that.
|
||||||
|
// lame.
|
||||||
|
initFormatHash(store)
|
||||||
|
registerFormatListener(store)
|
||||||
|
|
||||||
|
// detect hash parameter for markdown/plaintext format and initialize store
|
||||||
|
function initFormatHash(store) {
|
||||||
|
// get the has from the window location
|
||||||
|
let hash = window.location.hash.toUpperCase()
|
||||||
|
// strip the hash sign
|
||||||
|
hash = hash.substring(1)
|
||||||
|
|
||||||
|
// dispatch the appropriate action
|
||||||
|
if (hash === Format.MARKDOWN)
|
||||||
|
store.dispatch(Actions.setMarkdownFormat())
|
||||||
|
else if (hash === Format.PLAINTEXT || hash === '')
|
||||||
|
store.dispatch(Actions.setPlaintextFormat())
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen to changes in the redux store and update the url hash parameter when appropriate
|
||||||
|
function registerFormatListener(store) {
|
||||||
|
|
||||||
|
function handleChange() {
|
||||||
|
let nextFormat = store.getState().format;
|
||||||
|
|
||||||
|
if (nextFormat === Format.MARKDOWN)
|
||||||
|
window.history.replaceState("", document.title, window.location.pathname+"#"+nextFormat.toLowerCase());
|
||||||
|
else if (nextFormat === Format.PLAINTEXT) {
|
||||||
|
window.history.replaceState("", document.title, window.location.pathname);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsubscribe = store.subscribe(handleChange);
|
||||||
|
return unsubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function render() {
|
function render () {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<LocalStorage >
|
<LocalStorage >
|
||||||
<Router history={browserHistory}>
|
<Router history={browserHistory}>
|
||||||
{routes}
|
{routes}
|
||||||
</Router>
|
</Router>
|
||||||
</LocalStorage>
|
</LocalStorage>
|
||||||
</Provider>
|
</Provider>
|
||||||
, document.getElementById('root'))
|
, document.getElementById('root'))
|
||||||
}
|
}
|
||||||
|
|
||||||
render()
|
|
||||||
|
|
||||||
|
@ -3,84 +3,81 @@ import uuid from 'uuid/v4'
|
|||||||
import {browserHistory} from 'react-router'
|
import {browserHistory} from 'react-router'
|
||||||
import {Status, StatusError} from './constants'
|
import {Status, StatusError} from './constants'
|
||||||
|
|
||||||
//All state transitions in the app happen in these methods
|
// All state transitions in the app happen in these methods
|
||||||
//this includes redux state changes, asyncronous data requests, and browser location changes
|
// this includes redux state changes, asyncronous data requests, and browser location changes
|
||||||
|
|
||||||
export const updateOriginalInput = (text) =>
|
export const updateOriginalInput = (text) =>
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
dispatch({type: 'UPDATE_ORIGINAL_INPUT', data:text})
|
dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: text})
|
||||||
if (getState().input.original.length>0)
|
if (getState().input.original.length > 0) {
|
||||||
dispatch({type: 'STATUS_SET', data:Status.DIRTY})
|
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
|
||||||
else
|
} else {
|
||||||
dispatch({type: 'STATUS_SET', data:Status.EMPTY})
|
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateFinalInput = (text) =>
|
export const updateFinalInput = (text) =>
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
dispatch({ type: 'UPDATE_FINAL_INPUT', data:text})
|
dispatch({type: 'UPDATE_FINAL_INPUT', data: text})
|
||||||
if (getState().input.final.length>0)
|
if (getState().input.final.length > 0) {
|
||||||
dispatch({type: 'STATUS_SET', data:Status.DIRTY})
|
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
|
||||||
else
|
} else {
|
||||||
dispatch({type: 'STATUS_SET', data:Status.EMPTY})
|
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearInput = () =>
|
export const clearInput = () =>
|
||||||
(dispatch) => {
|
(dispatch) => {
|
||||||
dispatch({type: 'CLEAR_INPUT'})
|
dispatch({type: 'CLEAR_INPUT'})
|
||||||
dispatch({type: 'STATUS_SET', data:Status.EMPTY})
|
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT'})
|
export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT' })
|
||||||
export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT'})
|
export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT' })
|
||||||
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL'})
|
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL' })
|
||||||
export const showFinal = () => ({ type: 'SHOW_FINAL'})
|
export const showFinal = () => ({ type: 'SHOW_FINAL' })
|
||||||
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE'})
|
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE' })
|
||||||
|
|
||||||
|
// if the input is dirty, saves it to the server
|
||||||
//if the input is dirty, saves it to the server
|
// creates a new uuid for the same,
|
||||||
//creates a new uuid for the same,
|
// then changes the browser location to a comparison view with that id
|
||||||
//then changes the browser location to a comparison view with that id
|
export const compare = () =>
|
||||||
export const compare = () =>
|
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
//!!! could test that the input is dirty before triggering a save
|
//! !! could test that the input is dirty before triggering a save
|
||||||
//if the input is empty, the compare should do nothing
|
// if the input is empty, the compare should do nothing
|
||||||
//if the input is clean, the compare should not save and keep using the same id
|
// if the input is clean, the compare should not save and keep using the same id
|
||||||
|
|
||||||
//start saving the input to the server
|
// start saving the input to the server
|
||||||
const id = dispatch(save())
|
const id = dispatch(save())
|
||||||
|
|
||||||
//we can use the id created by the save method to build a path
|
// we can use the id created by the save method to build a path
|
||||||
const comparePath = `/${id}`
|
const comparePath = `/${id}`
|
||||||
browserHistory.replace(comparePath)
|
browserHistory.replace(comparePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear the input and return to the edit page
|
||||||
//clear the input and return to the edit page
|
export const reset = () =>
|
||||||
export const reset = () =>
|
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
dispatch(clearInput())
|
dispatch(clearInput())
|
||||||
browserHistory.push('/')
|
browserHistory.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// switch to the edit view
|
||||||
//switch to the edit view
|
export const edit = () =>
|
||||||
export const edit = () =>
|
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
browserHistory.push('/')
|
browserHistory.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saves the current input fields to the server
|
||||||
//saves the current input fields to the server
|
// creates and returns a new id for the comparison
|
||||||
//creates and returns a new id for the comparison
|
// should this method ensure that the initial state is valid? ('DIRTY')
|
||||||
//should this method ensure that the initial state is valid? ('DIRTY')
|
|
||||||
export const save = () =>
|
export const save = () =>
|
||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
|
// generate an id
|
||||||
//generate an id
|
|
||||||
const id = uuid()
|
const id = uuid()
|
||||||
|
|
||||||
//set waiting state
|
// set waiting state
|
||||||
dispatch( {type: 'STATUS_SET', data:Status.SAVING})
|
dispatch({type: 'STATUS_SET', data: Status.SAVING})
|
||||||
|
|
||||||
const endpointUri = `/api/compare/${id}`
|
const endpointUri = `/api/compare/${id}`
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
@ -90,32 +87,31 @@ export const save = () =>
|
|||||||
b: getState().input.final
|
b: getState().input.final
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dispatch post request
|
||||||
//dispatch post request
|
|
||||||
fetch(endpointUri, fetchOptions)
|
fetch(endpointUri, fetchOptions)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok)
|
if (response.ok) {
|
||||||
dispatch({type: 'STATUS_SET', data: Status.CLEAN})
|
dispatch({type: 'STATUS_SET', data: Status.CLEAN})
|
||||||
else {
|
} else {
|
||||||
response.text().then( (responseText) => {
|
response.text().then((responseText) => {
|
||||||
const error = {message:`${response.status}: ${responseText}`}
|
const error = {message: `${response.status}: ${responseText}`}
|
||||||
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
|
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
|
||||||
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, error})
|
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, error})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
//!!! could use a better error message here
|
//! !! could use a better error message here
|
||||||
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
|
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
|
||||||
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, error})
|
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, error})
|
||||||
})
|
})
|
||||||
|
|
||||||
//return the id after the request has been sent
|
// return the id after the request has been sent
|
||||||
return id;
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -130,7 +126,6 @@ const load = (id) =>
|
|||||||
method: 'GET'
|
method: 'GET'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//dispatch post request
|
//dispatch post request
|
||||||
fetch(endpointUri, fetchOptions)
|
fetch(endpointUri, fetchOptions)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@ -152,4 +147,4 @@ export const loadIfNeeded = (id) =>
|
|||||||
(dispatch, getState) => {
|
(dispatch, getState) => {
|
||||||
if
|
if
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
import {Segment, Grid, Form} from 'semantic-ui-react'
|
import {Segment, Grid} from 'semantic-ui-react'
|
||||||
|
|
||||||
import * as Actions from '../actions'
|
|
||||||
import * as Selectors from '../selectors'
|
import * as Selectors from '../selectors'
|
||||||
|
|
||||||
|
import {Format} from '../constants'
|
||||||
|
|
||||||
import Header from './Header'
|
import Header from './Header'
|
||||||
import Footer from './Footer'
|
import Footer from './Footer'
|
||||||
import CompareControls from './CompareControls'
|
import CompareControls from './CompareControls'
|
||||||
@ -15,19 +16,17 @@ import ShowMarkdown from './ShowMarkdown'
|
|||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
isMarkdownFormat: Selectors.isMarkdownFormat(state),
|
isMarkdownFormat: Selectors.isMarkdownFormat(state),
|
||||||
isShowOriginal: Selectors.isShowOriginal(state),
|
isShowOriginal: Selectors.isShowOriginal(state),
|
||||||
isShowFinal: Selectors.isShowFinal(state),
|
isShowFinal: Selectors.isShowFinal(state),
|
||||||
isShowDifference: Selectors.isShowDifference(state),
|
isShowDifference: Selectors.isShowDifference(state),
|
||||||
safeInput: Selectors.safeInput(state),
|
safeInput: Selectors.safeInput(state),
|
||||||
diff: Selectors.diff(state)
|
diff: Selectors.diff(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
//loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
|
// loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Compare extends React.Component {
|
class Compare extends React.Component {
|
||||||
/*
|
/*
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -35,39 +34,39 @@ class Compare extends React.Component {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
render() {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header/>
|
<Header />
|
||||||
|
|
||||||
<Segment basic padded>
|
<Segment basic padded>
|
||||||
<Grid stackable columns={2}>
|
<Grid stackable columns={2}>
|
||||||
<Grid.Column width="3">
|
<Grid.Column width='3'>
|
||||||
<CompareControls/>
|
<CompareControls />
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column width="13">
|
<Grid.Column width='13'>
|
||||||
<Segment>
|
<Segment>
|
||||||
{
|
{
|
||||||
(!this.props.isMarkdownFormat && this.props.isShowDifference) ?
|
(!this.props.isMarkdownFormat && this.props.isShowDifference)
|
||||||
<ShowPlaintext diff={this.props.diff}>{this.props.diff}</ShowPlaintext>:
|
? <ShowPlaintext diff={this.props.diff}>{this.props.diff}</ShowPlaintext>
|
||||||
(this.props.isMarkdownFormat && this.props.isShowDifference) ?
|
: (this.props.isMarkdownFormat && this.props.isShowDifference)
|
||||||
<ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>:
|
? <ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>
|
||||||
(!this.props.isMarkdownFormat && !this.props.isShowDifference) ?
|
: (!this.props.isMarkdownFormat && !this.props.isShowDifference)
|
||||||
<ShowPlaintext
|
? <ShowPlaintext
|
||||||
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
|
text={this.props.isShowOriginal ? this.props.safeInput.original : this.props.safeInput.final}
|
||||||
/> :
|
/>
|
||||||
(this.props.isMarkdownFormat && !this.props.isShowDifference) ?
|
: (this.props.isMarkdownFormat && !this.props.isShowDifference)
|
||||||
<ShowMarkdown
|
? <ShowMarkdown
|
||||||
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
|
text={this.props.isShowOriginal ? this.props.safeInput.original : this.props.safeInput.final}
|
||||||
/> :
|
/>
|
||||||
null
|
: null
|
||||||
}
|
}
|
||||||
</Segment>
|
</Segment>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
<Footer/>
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -75,10 +74,9 @@ class Compare extends React.Component {
|
|||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Compare)
|
export default connect(mapStateToProps, mapDispatchToProps)(Compare)
|
||||||
|
|
||||||
|
|
||||||
/* <div ng-if="isMarkdownFormat">
|
/* <div ng-if="isMarkdownFormat">
|
||||||
<div ng-show="isShowBefore" class="col-md-10 col-sm-12 content-well">
|
<div ng-show="isShowBefore" class="col-md-10 col-sm-12 content-well">
|
||||||
<div btf-markdown="before" class="before">
|
<div btf-markdown="before" class="before">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="isShowWdiff" class="col-md-10 col-sm-12 content-well">
|
<div ng-show="isShowWdiff" class="col-md-10 col-sm-12 content-well">
|
||||||
@ -101,4 +99,4 @@ export default connect(mapStateToProps, mapDispatchToProps)(Compare)
|
|||||||
<div ng-bind-html="after" class="content-pre after"></div>
|
<div ng-bind-html="after" class="content-pre after"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {Link} from 'react-router'
|
|
||||||
|
|
||||||
import {Button, Icon, Segment} from 'semantic-ui-react'
|
import {Button, Icon, Segment} from 'semantic-ui-react'
|
||||||
|
|
||||||
@ -11,13 +10,12 @@ const mapStateToProps = (state) => ({
|
|||||||
isMarkdownFormat: Selectors.isMarkdownFormat(state),
|
isMarkdownFormat: Selectors.isMarkdownFormat(state),
|
||||||
isShowOriginal: Selectors.isShowOriginal(state),
|
isShowOriginal: Selectors.isShowOriginal(state),
|
||||||
isShowFinal: Selectors.isShowFinal(state),
|
isShowFinal: Selectors.isShowFinal(state),
|
||||||
isShowDifference: Selectors.isShowDifference(state),
|
isShowDifference: Selectors.isShowDifference(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onSetPlaintextFormat: () => dispatch(Actions.setPlaintextFormat()),
|
onSetPlaintextFormat: () => dispatch(Actions.setPlaintextFormat()),
|
||||||
onSetMarkdownFormat: () => dispatch(Actions.setMarkdownFormat()),
|
onSetMarkdownFormat: () => dispatch(Actions.setMarkdownFormat()),
|
||||||
onShowOriginal: () => dispatch(Actions.showOriginal()),
|
onShowOriginal: () => dispatch(Actions.showOriginal()),
|
||||||
onShowFinal: () => dispatch(Actions.showFinal()),
|
onShowFinal: () => dispatch(Actions.showFinal()),
|
||||||
onShowDifference: () => dispatch(Actions.showDifference()),
|
onShowDifference: () => dispatch(Actions.showDifference()),
|
||||||
@ -26,19 +24,19 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
|
|
||||||
class CompareControls extends React.Component {
|
class CompareControls extends React.Component {
|
||||||
|
|
||||||
onClickMarkdownFormat() {
|
onClickMarkdownFormat () {
|
||||||
if (this.props.isMarkdownFormat)
|
if (this.props.isMarkdownFormat) {
|
||||||
this.props.onSetPlaintextFormat()
|
this.props.onSetPlaintextFormat()
|
||||||
else
|
} else {
|
||||||
this.props.onSetMarkdownFormat()
|
this.props.onSetMarkdownFormat()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<Segment.Group>
|
<Segment.Group>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Button fluid onClick={this.props.onEdit}>Edit</Button>
|
<Button fluid onClick={this.props.onEdit}>Edit</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
<Segment >
|
<Segment >
|
||||||
@ -48,8 +46,8 @@ class CompareControls extends React.Component {
|
|||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
<Segment >
|
<Segment >
|
||||||
<Button fluid active={this.props.isMarkdownFormat} type="submit" onClick={this.onClickMarkdownFormat.bind(this)}>
|
<Button fluid active={this.props.isMarkdownFormat} type='submit' onClick={this.onClickMarkdownFormat.bind(this)}>
|
||||||
{this.props.isMarkdownFormat ? <Icon name="checkmark"/> : <span/>}
|
{this.props.isMarkdownFormat ? <Icon name='checkmark' /> : <span />}
|
||||||
As Markdown
|
As Markdown
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
@ -3,9 +3,9 @@ import React from 'react'
|
|||||||
import {Segment} from 'semantic-ui-react'
|
import {Segment} from 'semantic-ui-react'
|
||||||
|
|
||||||
const Footer = (props) => (
|
const Footer = (props) => (
|
||||||
<Segment basic padded textAlign="center" as="footer">
|
<Segment basic padded textAlign='center' as='footer'>
|
||||||
<p><a href="http://adamarthurryan.com">Adam Brown</a> | This website is <a href="https://github.com/adamarthurryan/dubdiff">open source</a>.</p>
|
<p><a href='http://adamarthurryan.com'>Adam Brown</a> | This website is <a href='https://github.com/adamarthurryan/dubdiff'>open source</a>.</p>
|
||||||
</Segment>
|
</Segment>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default Footer
|
export default Footer
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
import {Segment, Header, Rail, Container} from 'semantic-ui-react'
|
import {Segment, Header, Rail} from 'semantic-ui-react'
|
||||||
import {Link} from 'react-router'
|
import {Link} from 'react-router'
|
||||||
|
|
||||||
import * as Actions from '../actions'
|
import * as Actions from '../actions'
|
||||||
@ -10,27 +10,25 @@ import SaveStatus from './SaveStatus'
|
|||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onReset: () => { console.log(Actions.reset()); dispatch(Actions.reset())},
|
onReset: () => { console.log(Actions.reset()); dispatch(Actions.reset()) }
|
||||||
})
|
})
|
||||||
|
|
||||||
const SiteHeader = (props) => (
|
const SiteHeader = (props) => (
|
||||||
|
|
||||||
|
<Segment basic >
|
||||||
<Segment basic >
|
|
||||||
|
|
||||||
<Segment basic padded textAlign="center" as="header" id='masthead'>
|
|
||||||
<Header><Link onClick={props.onReset}>dubdiff</Link></Header>
|
|
||||||
</Segment>
|
|
||||||
|
|
||||||
<Rail internal position="right">
|
|
||||||
<Segment basic padded>
|
|
||||||
<SaveStatus/>
|
|
||||||
</Segment>
|
|
||||||
</Rail>
|
|
||||||
|
|
||||||
|
<Segment basic padded textAlign='center' as='header' id='masthead'>
|
||||||
|
<Header><Link onClick={props.onReset}>dubdiff</Link></Header>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
|
<Rail internal position='right'>
|
||||||
|
<Segment basic padded>
|
||||||
|
<SaveStatus />
|
||||||
|
</Segment>
|
||||||
|
</Rail>
|
||||||
|
|
||||||
|
</Segment>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SiteHeader)
|
export default connect(mapStateToProps, mapDispatchToProps)(SiteHeader)
|
||||||
|
@ -13,59 +13,50 @@ import MainControls from './MainControls'
|
|||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
input: state.input,
|
input: state.input,
|
||||||
safeInput: Selectors.safeInput(state),
|
safeInput: Selectors.safeInput(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
|
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
|
||||||
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text)),
|
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header/>
|
<Header />
|
||||||
|
|
||||||
<Segment basic padded>
|
<Segment basic padded>
|
||||||
<Grid stackable columns={3}>
|
<Grid stackable columns={3}>
|
||||||
<Grid.Column width="3">
|
<Grid.Column width='3'>
|
||||||
<MainControls/>
|
<MainControls />
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column width="6">
|
<Grid.Column width='6'>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Original</label>
|
<label>Original</label>
|
||||||
<textarea value={this.props.input.original} onChange={event => this.props.onChangeOriginal(event.target.value)}></textarea>
|
<textarea value={this.props.input.original} onChange={event => this.props.onChangeOriginal(event.target.value)} />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column width="6">
|
<Grid.Column width='6'>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Final</label>
|
<label>Final</label>
|
||||||
<textarea value={this.props.input.final} onChange={event => this.props.onChangeFinal(event.target.value)}></textarea>
|
<textarea value={this.props.input.final} onChange={event => this.props.onChangeFinal(event.target.value)} />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
<Footer/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Main)
|
export default connect(mapStateToProps, mapDispatchToProps)(Main)
|
||||||
|
@ -7,31 +7,30 @@ import * as Actions from '../actions'
|
|||||||
import * as Selectors from '../selectors'
|
import * as Selectors from '../selectors'
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => ({
|
||||||
format: state.format,
|
format: state.format,
|
||||||
isMarkdownFormat: Selectors.isMarkdownFormat(state),
|
isMarkdownFormat: Selectors.isMarkdownFormat(state),
|
||||||
saveStatus: state.saveStatus
|
saveStatus: state.saveStatus
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()),
|
onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()),
|
||||||
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
|
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
|
||||||
|
|
||||||
//returns an id for the record to be saved
|
// returns an id for the record to be saved
|
||||||
onCompare: () => dispatch(Actions.compare())
|
onCompare: () => dispatch(Actions.compare())
|
||||||
})
|
})
|
||||||
|
|
||||||
class MainControls extends React.Component {
|
class MainControls extends React.Component {
|
||||||
|
|
||||||
onClickMarkdownFormat() {
|
onClickMarkdownFormat () {
|
||||||
if (this.props.isMarkdownFormat)
|
if (this.props.isMarkdownFormat) {
|
||||||
this.props.onSetPlaintextFormat()
|
this.props.onSetPlaintextFormat()
|
||||||
else
|
} else {
|
||||||
this.props.onSetMarkdownFormat()
|
this.props.onSetMarkdownFormat()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<Segment.Group>
|
<Segment.Group>
|
||||||
<Segment >
|
<Segment >
|
||||||
@ -39,8 +38,8 @@ class MainControls extends React.Component {
|
|||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
<Segment >
|
<Segment >
|
||||||
<Button fluid active={this.props.isMarkdownFormat} type="submit" onClick={this.onClickMarkdownFormat.bind(this)}>
|
<Button fluid active={this.props.isMarkdownFormat} type='submit' onClick={this.onClickMarkdownFormat.bind(this)}>
|
||||||
{this.props.isMarkdownFormat ? <Icon name="checkmark"/> : <span/>}
|
{this.props.isMarkdownFormat ? <Icon name='checkmark' /> : <span />}
|
||||||
As Markdown
|
As Markdown
|
||||||
</Button>
|
</Button>
|
||||||
</Segment>
|
</Segment>
|
||||||
@ -52,4 +51,4 @@ class MainControls extends React.Component {
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(MainControls)
|
export default connect(mapStateToProps, mapDispatchToProps)(MainControls)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
<a type="button" onClick={this.onClickCompare.bind(this)} className="btn btn-block btn-primary">compare</a>*/
|
<a type="button" onClick={this.onClickCompare.bind(this)} className="btn btn-block btn-primary">compare</a> */
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
import { Message, Icon, Button} from 'semantic-ui-react'
|
import {Message, Icon, Button} from 'semantic-ui-react'
|
||||||
import { browserHistory} from 'react-router'
|
|
||||||
|
|
||||||
import * as Actions from '../actions'
|
import * as Actions from '../actions'
|
||||||
import {Status, StatusError} from '../constants'
|
import {Status, StatusError} from '../constants'
|
||||||
@ -11,54 +10,56 @@ const mapStateToProps = (state) => ({
|
|||||||
status: state.status
|
status: state.status
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onSave: () => dispatch(Actions.save()),
|
onSave: () => dispatch(Actions.save()),
|
||||||
onReset: () => dispatch(Actions.reset())
|
onReset: () => dispatch(Actions.reset())
|
||||||
})
|
})
|
||||||
|
|
||||||
const SaveStatus = (props) => {
|
const SaveStatus = (props) => {
|
||||||
if (props.status.type == Status.SAVING) return (
|
if (props.status.type === Status.SAVING) {
|
||||||
<Message size='tiny' floating compact icon>
|
return (
|
||||||
<Icon name='circle notched' loading />
|
<Message size='tiny' floating compact icon>
|
||||||
<Message.Content>
|
<Icon name='circle notched' loading />
|
||||||
<Message.Header>Saving diff</Message.Header>
|
<Message.Content>
|
||||||
</Message.Content>
|
<Message.Header>Saving diff</Message.Header>
|
||||||
</Message>
|
</Message.Content>
|
||||||
)
|
</Message>
|
||||||
if (props.status.type == Status.LOADING) return (
|
)
|
||||||
<Message size='tiny' floating compact icon>
|
}
|
||||||
<Icon name='circle notched' loading />
|
if (props.status.type === Status.LOADING) {
|
||||||
<Message.Content>
|
return (
|
||||||
<Message.Header>Loading diff</Message.Header>
|
<Message size='tiny' floating compact icon>
|
||||||
</Message.Content>
|
<Icon name='circle notched' loading />
|
||||||
</Message>
|
<Message.Content>
|
||||||
)
|
<Message.Header>Loading diff</Message.Header>
|
||||||
else if (props.status.hasError && props.status.errorType == StatusError.SAVE_ERROR) return (
|
</Message.Content>
|
||||||
<Message size='tiny' floating compact icon>
|
</Message>
|
||||||
<Icon name='exclamation' />
|
)
|
||||||
<Message.Content>
|
} else if (props.status.hasError && props.status.errorType === StatusError.SAVE_ERROR) {
|
||||||
<Message.Header>Error saving diff</Message.Header>
|
return (
|
||||||
{props.status.error.message}
|
<Message size='tiny' floating compact icon>
|
||||||
<br/>
|
<Icon name='exclamation' />
|
||||||
<Button onClick={props.onSave}>Retry</Button>
|
<Message.Content>
|
||||||
</Message.Content>
|
<Message.Header>Error saving diff</Message.Header>
|
||||||
</Message>
|
{props.status.error.message}
|
||||||
)
|
<br />
|
||||||
else if (props.status.hasError && props.status.errorType == StatusError.LOAD_ERROR) return (
|
<Button onClick={props.onSave}>Retry</Button>
|
||||||
<Message size='tiny' floating compact icon>
|
</Message.Content>
|
||||||
<Icon name='exclamation' />
|
</Message>
|
||||||
<Message.Content>
|
)
|
||||||
<Message.Header>Error loading diff</Message.Header>
|
} else if (props.status.hasError && props.status.errorType === StatusError.LOAD_ERROR) {
|
||||||
{props.status.error.message}
|
return (
|
||||||
<br/>
|
<Message size='tiny' floating compact icon>
|
||||||
<Button onClick={props.onReset}>New Diff</Button>
|
<Icon name='exclamation' />
|
||||||
</Message.Content>
|
<Message.Content>
|
||||||
</Message>
|
<Message.Header>Error loading diff</Message.Header>
|
||||||
)
|
{props.status.error.message}
|
||||||
|
<br />
|
||||||
else return ( <div></div> )
|
<Button onClick={props.onReset}>New Diff</Button>
|
||||||
|
</Message.Content>
|
||||||
|
</Message>
|
||||||
|
)
|
||||||
|
} else return (<div />)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus)
|
export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus)
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import markdownCompiler from 'markdown-to-jsx'
|
import markdownCompiler from 'markdown-to-jsx'
|
||||||
|
|
||||||
import {diffToString, diffToHtml} from '../util/dubdiff'
|
import {diffToHtml} from '../util/dubdiff'
|
||||||
|
|
||||||
const ShowMarkdown = (props) => {
|
const ShowMarkdown = (props) => {
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{
|
{
|
||||||
props.text ?
|
props.text
|
||||||
markdownCompiler(props.text) :
|
? markdownCompiler(props.text)
|
||||||
props.diff ?
|
: props.diff
|
||||||
markdownCompiler(diffToHtml(props.diff)) :
|
? markdownCompiler(diffToHtml(props.diff))
|
||||||
null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
||||||
const ShowPlaintext = (props) => {
|
const ShowPlaintext = (props) => {
|
||||||
return <div>
|
return <div>
|
||||||
<pre style={{whiteSpace:'pre-wrap'}}>
|
<pre style={{whiteSpace: 'pre-wrap'}}>
|
||||||
{props.text ?
|
{props.text
|
||||||
props.text:
|
? props.text
|
||||||
props.diff ?
|
: props.diff
|
||||||
diffToPre(props.diff) :
|
? diffToPre(props.diff)
|
||||||
null
|
: null
|
||||||
}
|
}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -16,10 +15,12 @@ const ShowPlaintext = (props) => {
|
|||||||
|
|
||||||
export default ShowPlaintext
|
export default ShowPlaintext
|
||||||
|
|
||||||
function diffToPre(diff) {
|
function diffToPre (diff) {
|
||||||
return diff.map((part, index) => (
|
return diff.map((part, index) => (
|
||||||
part.added ? <ins key={index}>{part.value}</ins> :
|
part.added
|
||||||
part.removed ? <del key={index}>{part.value}</del> :
|
? <ins key={index}>{part.value}</ins>
|
||||||
<span key={index}>{part.value}</span>
|
: part.removed
|
||||||
|
? <del key={index}>{part.value}</del>
|
||||||
|
: <span key={index}>{part.value}</span>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,9 @@ export const Format = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Show = {
|
export const Show = {
|
||||||
ORIGINAL:'ORIGINAL',
|
ORIGINAL: 'ORIGINAL',
|
||||||
FINAL:'FINAL',
|
FINAL: 'FINAL',
|
||||||
DIFFERENCE:'DIFFERENCE'
|
DIFFERENCE: 'DIFFERENCE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Status = {
|
export const Status = {
|
||||||
@ -21,4 +21,4 @@ export const Status = {
|
|||||||
export const StatusError = {
|
export const StatusError = {
|
||||||
LOAD_ERROR: 'LOAD_ERROR',
|
LOAD_ERROR: 'LOAD_ERROR',
|
||||||
SAVE_ERROR: 'SAVE_ERROR'
|
SAVE_ERROR: 'SAVE_ERROR'
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import {Format, Show, Status, StatusError} from './constants'
|
import {Format, Show, Status, StatusError} from './constants'
|
||||||
|
|
||||||
|
export function input (state, action) {
|
||||||
export function input (state, action ) {
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'UPDATE_ORIGINAL_INPUT':
|
case 'UPDATE_ORIGINAL_INPUT':
|
||||||
return Object.assign({}, state, {original:action.data})
|
return Object.assign({}, state, {original: action.data})
|
||||||
case 'UPDATE_FINAL_INPUT':
|
case 'UPDATE_FINAL_INPUT':
|
||||||
return Object.assign({}, state, {final:action.data})
|
return Object.assign({}, state, {final: action.data})
|
||||||
case 'CLEAR_INPUT':
|
case 'CLEAR_INPUT':
|
||||||
return {original:'', final:''}
|
return {original: '', final: ''}
|
||||||
default:
|
default:
|
||||||
return state || {original:'', final:''}
|
return state || {original: '', final: ''}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,8 +22,7 @@ export function format (state, action) {
|
|||||||
default:
|
default:
|
||||||
return state || Format.PLAINTEXT
|
return state || Format.PLAINTEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function show (state, action) {
|
export function show (state, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -58,18 +56,20 @@ export function saveStatus (state, action) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//tracks status of the app, especially with respect to loaded and saved user data
|
// tracks status of the app, especially with respect to loaded and saved user data
|
||||||
export function status (state, action) {
|
export function status (state, action) {
|
||||||
//the status or error type is valid if it is in the list of Status or StatusError types
|
// the status or error type is valid if it is in the list of Status or StatusError types
|
||||||
const isValidStatus = (type) => Status[type] == type
|
const isValidStatus = (type) => Status[type] === type
|
||||||
const isValidError = (type) => StatusError[type] == type
|
const isValidError = (type) => StatusError[type] === type
|
||||||
|
|
||||||
//the error is cleared when status changes
|
// the error is cleared when status changes
|
||||||
if (action.type == 'STATUS_SET' && isValidStatus(action.data))
|
if (action.type === 'STATUS_SET' && isValidStatus(action.data)) {
|
||||||
return {type:action.data, error: null, hasError: false, errorType: null}
|
return {type: action.data, error: null, hasError: false, errorType: null}
|
||||||
//the error is set in addition to the status
|
}
|
||||||
else if (action.type == 'STATUS_SET_ERROR' && isValidError(action.data))
|
// the error is set in addition to the status
|
||||||
return Object.assign({}, state, {error: action.error, hasError: true, errorType:action.data})
|
else if (action.type === 'STATUS_SET_ERROR' && isValidError(action.data)) {
|
||||||
else
|
return Object.assign({}, state, {error: action.error, hasError: true, errorType: action.data})
|
||||||
return state || {type:Status.EMPTY, hasError: false, error:null}
|
} else {
|
||||||
|
return state || {type: Status.EMPTY, hasError: false, error: null}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {Route, IndexRout, Redirect } from 'react-router'
|
import {Route} from 'react-router'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
|
|
||||||
import Main from './components/Main'
|
import Main from './components/Main'
|
||||||
import Compare from './components/Compare'
|
import Compare from './components/Compare'
|
||||||
|
|
||||||
var routes = [
|
var routes = [
|
||||||
<Route key="root" path="/" component={Main}/>,
|
<Route key='root' path='/' component={Main} />,
|
||||||
<Route key="compare" path="/:compareId" component={Compare}/>
|
<Route key='compare' path='/:compareId' component={Compare} />
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
//per http://redux.js.org/docs/recipes/ComputingDerivedData.html
|
// per http://redux.js.org/docs/recipes/ComputingDerivedData.html
|
||||||
|
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
||||||
import {Format, Show} from './constants'
|
import {Format, Show} from './constants'
|
||||||
|
|
||||||
import * as Dubdiff from './util/dubdiff'
|
import * as Dubdiff from './util/dubdiff'
|
||||||
|
|
||||||
|
|
||||||
const input = (state) => state.input
|
const input = (state) => state.input
|
||||||
const format = (state) => state.format
|
const format = (state) => state.format
|
||||||
const show = (state) => state.show
|
const show = (state) => state.show
|
||||||
|
|
||||||
export const safeInput = createSelector(
|
export const safeInput = createSelector(
|
||||||
[input],
|
[input],
|
||||||
(input) => {
|
(input) => {
|
||||||
//!!! sanitize the input here and return
|
//! !! sanitize the input here and return
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -25,44 +23,37 @@ export const safeInput = createSelector(
|
|||||||
export const isMarkdownFormat = createSelector(
|
export const isMarkdownFormat = createSelector(
|
||||||
[format],
|
[format],
|
||||||
(format) => {
|
(format) => {
|
||||||
return format == Format.MARKDOWN
|
return format === Format.MARKDOWN
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const isShow = (type) => createSelector(
|
const isShow = (type) => createSelector(
|
||||||
[show],
|
[show],
|
||||||
(show) => {
|
(show) => {
|
||||||
return show == type
|
return show === type
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const isShowOriginal = isShow(Show.ORIGINAL)
|
export const isShowOriginal = isShow(Show.ORIGINAL)
|
||||||
export const isShowFinal = isShow(Show.FINAL)
|
export const isShowFinal = isShow(Show.FINAL)
|
||||||
export const isShowDifference= isShow(Show.DIFFERENCE)
|
export const isShowDifference = isShow(Show.DIFFERENCE)
|
||||||
|
|
||||||
|
|
||||||
export const diff = createSelector(
|
export const diff = createSelector(
|
||||||
[format, safeInput],
|
[format, safeInput],
|
||||||
(format, safeInput) => {
|
(format, safeInput) => {
|
||||||
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
|
if (format === Format.PLAINTEXT) {
|
||||||
/*
|
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
|
||||||
let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' '))
|
} else if (format === Format.MARKDOWN) {
|
||||||
return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => (
|
return Dubdiff.markdownDiff(safeInput.original, safeInput.final)
|
||||||
part.added ? <ins>{part.value}</ins> :
|
}
|
||||||
part.removed ? <del>{part.value}</del> :
|
|
||||||
<span>{part.value}</span>
|
|
||||||
))
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
html diff
|
html diff
|
||||||
---
|
---
|
||||||
|
|
||||||
diffHtml(parentOriginal, parentFinal) {
|
diffHtml(parentOriginal, parentFinal) {
|
||||||
create stringOriginal, stringFinal consisting of
|
create stringOriginal, stringFinal consisting of
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
@ -6,44 +6,42 @@ import {Diff} from 'diff'
|
|||||||
// but is preserved and included in the output.
|
// but is preserved and included in the output.
|
||||||
|
|
||||||
const TOKEN_BOUNDARYS = /([\s,.:])/
|
const TOKEN_BOUNDARYS = /([\s,.:])/
|
||||||
|
|
||||||
class EditorsDiff extends Diff {
|
class EditorsDiff extends Diff {
|
||||||
constructor (tokenBoundaries=TOKEN_BOUNDARYS) {
|
constructor (tokenBoundaries = TOKEN_BOUNDARYS) {
|
||||||
super()
|
super()
|
||||||
this.tokenBoundaries = tokenBoundaries
|
this.tokenBoundaries = tokenBoundaries
|
||||||
}
|
}
|
||||||
|
|
||||||
equals (left, right) {
|
equals (left, right) {
|
||||||
return (
|
return (
|
||||||
left.string == right.string
|
left.string === right.string
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splits the input string into a series of word and punctuation tokens
|
||||||
//splits the input string into a series of word and punctuation tokens
|
// each token is associated with an optional trailing array of spaces
|
||||||
//each token is associated with an optional trailing array of spaces
|
|
||||||
tokenize (value) {
|
tokenize (value) {
|
||||||
let tokens = value.split(this.tokenBoundaries)
|
let tokens = value.split(this.tokenBoundaries)
|
||||||
let annotatedTokens = []
|
let annotatedTokens = []
|
||||||
tokens.forEach( token => {
|
tokens.forEach(token => {
|
||||||
if (isSpace(token)) {
|
if (isSpace(token)) {
|
||||||
if (annotatedTokens.length == 0)
|
if (annotatedTokens.length === 0) {
|
||||||
annotatedTokens.push({string:'', whitespace:[]})
|
annotatedTokens.push({string: '', whitespace: []})
|
||||||
|
}
|
||||||
|
|
||||||
let last = annotatedTokens[annotatedTokens.length-1]
|
let last = annotatedTokens[annotatedTokens.length - 1]
|
||||||
last.whitespace.push(token)
|
last.whitespace.push(token)
|
||||||
}
|
} else {
|
||||||
else {
|
annotatedTokens.push({string: token, whitespace: []})
|
||||||
annotatedTokens.push({string:token, whitespace:[]})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
//this final empty token is necessary for the jsdiff diffing engine to work properly
|
// this final empty token is necessary for the jsdiff diffing engine to work properly
|
||||||
annotatedTokens.push({string:'', whitespace:[]})
|
annotatedTokens.push({string: '', whitespace: []})
|
||||||
return annotatedTokens
|
return annotatedTokens
|
||||||
}
|
}
|
||||||
join(annotatedTokens) {
|
join (annotatedTokens) {
|
||||||
let tokens = []
|
let tokens = []
|
||||||
annotatedTokens.forEach(annotatedToken => {
|
annotatedTokens.forEach(annotatedToken => {
|
||||||
tokens.push(annotatedToken.string)
|
tokens.push(annotatedToken.string)
|
||||||
@ -53,11 +51,9 @@ class EditorsDiff extends Diff {
|
|||||||
})
|
})
|
||||||
return tokens.join('')
|
return tokens.join('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default EditorsDiff
|
export default EditorsDiff
|
||||||
|
|
||||||
const isSpace = str => /[ ]+/.test(str)
|
const isSpace = str => /[ ]+/.test(str)
|
||||||
|
const isNewline = str => /[\n]+/.test(str)
|
||||||
|
@ -1,115 +1,115 @@
|
|||||||
import * as JsDiff from 'diff'
|
|
||||||
import EditorsDiff from './EditorsDiff'
|
import EditorsDiff from './EditorsDiff'
|
||||||
|
|
||||||
let plaintextDiffer = new EditorsDiff()
|
let plaintextDiffer = new EditorsDiff()
|
||||||
let markdownDiffer = new EditorsDiff(/([\s,.:]|[*\[\]\(\)])/)
|
let markdownDiffer = new EditorsDiff(/([\s,.:]|[*\[\]()])/)
|
||||||
|
|
||||||
//returns a comparison of the texts as plaintext
|
// returns a comparison of the texts as plaintext
|
||||||
export function plaintextDiff(original, final) {
|
export function plaintextDiff (original, final) {
|
||||||
let diff = plaintextDiffer.diff(original, final)
|
let diff = plaintextDiffer.diff(original, final)
|
||||||
return diff
|
return diff
|
||||||
}
|
}
|
||||||
|
|
||||||
//returns a comparison of the texts as markdown
|
// returns a comparison of the texts as markdown
|
||||||
export function markdownDiff(original, final) {
|
export function markdownDiff (original, final) {
|
||||||
let diff = markdownDiffer.diff(original, final)
|
let diff = markdownDiffer.diff(original, final)
|
||||||
diff = rewriteMarkdownDiff(diff)
|
diff = rewriteMarkdownDiff(diff)
|
||||||
|
|
||||||
return diff
|
return diff
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a string version of the diff, with "{+ ... +}" and "[- ... -]"
|
// returns a string version of the diff, with "{+ ... +}" and "[- ... -]"
|
||||||
// representing ins and del blocks
|
// representing ins and del blocks
|
||||||
export function diffToString(diff, tags={added:{start:'{+', end:'+}'}, removed:{start:'[-', end:'-]'}, same:{start:'', end:''}}) {
|
export function diffToString (diff, tags = {added: {start: '{+', end: '+}'}, removed: {start: '[-', end: '-]'}, same: {start: '', end: ''}}) {
|
||||||
|
|
||||||
return diff.map(({added, removed, value}) => {
|
return diff.map(({added, removed, value}) => {
|
||||||
|
let {start, end} = added ? tags.added : (removed ? tags.removed : tags.same)
|
||||||
|
|
||||||
let {start,end} = added ? tags.added : (removed ? tags.removed : tags.same)
|
let string = value
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
string = value.join('')
|
||||||
|
}
|
||||||
|
|
||||||
let string = value
|
return start + string + end
|
||||||
if (Array.isArray(value))
|
|
||||||
string = value.join('')
|
|
||||||
|
|
||||||
return start+string+end
|
|
||||||
}).join('')
|
}).join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function diffToHtml(diff) {
|
export function diffToHtml (diff) {
|
||||||
return diffToString(diff, {added:{start:'<ins>', end:'</ins>'}, removed:{start:'<del>', end:'</del>'}, same:{start:'', end:''}})
|
return diffToString(diff, {added: {start: '<ins>', end: '</ins>'}, removed: {start: '<del>', end: '</del>'}, same: {start: '', end: ''}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Rewrites the given diff to correctly render as markdown, assuming the source
|
// Rewrites the given diff to correctly render as markdown, assuming the source
|
||||||
// documents were also valid markdown.
|
// documents were also valid markdown.
|
||||||
// In essence, moves the markdown formatting elements in or out of the inserted and deleted blocks, as appropriate
|
// In essence, moves the markdown formatting elements in or out of the inserted and deleted blocks, as appropriate
|
||||||
|
|
||||||
//rules:
|
// rules:
|
||||||
// 1. if a multiline del block is followed by an ins block,
|
|
||||||
|
// 1. if a multiline del block is followed by an ins block,
|
||||||
// the first line of the ins block should be inserted at the end of the first line of the del block
|
// the first line of the ins block should be inserted at the end of the first line of the del block
|
||||||
// so the markdown will apply to the ins text as it should
|
// so the markdown will apply to the ins text as it should
|
||||||
// 2. after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
|
// 2. multiline ins and del blocks should be broken up into a series of single line blocks
|
||||||
|
// 3. after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
|
||||||
// then that prefix should be moved out of the block
|
// then that prefix should be moved out of the block
|
||||||
|
|
||||||
//not yet implemented rules:
|
// not yet implemented rules:
|
||||||
// 3. if an ins or del block spans one half of a bold, italic or link string
|
// 3. if an ins or del block spans one half of a bold, italic or link string
|
||||||
// eg. **Hello <del>World** I</del><ins>Darling** she</ins> said
|
// eg. **Hello <del>World** I</del><ins>Darling** she</ins> said
|
||||||
// the block should be broken up to move the formatting code outside
|
// the block should be broken up to move the formatting code outside
|
||||||
// OR the whole formatting string could be brought into the block
|
// OR the whole formatting string could be brought into the block
|
||||||
// eg. **Hello <del>World</del><ins>Darling</ins>** <ins>I</ins><del>she</del> said
|
// eg. **Hello <del>World</del><ins>Darling</ins>** <ins>I</ins><del>she</del> said
|
||||||
function rewriteMarkdownDiff(diff) {
|
function rewriteMarkdownDiff (diff) {
|
||||||
//apply transformation rules
|
// apply transformation rules
|
||||||
let transformedDiff = diff
|
let transformedDiff = diff
|
||||||
transformedDiff= applyTransformationRule1(transformedDiff)
|
transformedDiff = applyTransformationRuleMultilineDelThenIns(transformedDiff)
|
||||||
transformedDiff= applyTransformationRule2(transformedDiff)
|
transformedDiff = applyTransformationRuleBreakUpDelIns(transformedDiff)
|
||||||
|
transformedDiff = applyTransformationRuleFormattingPrefix(transformedDiff)
|
||||||
|
transformedDiff = applyTransformationRuleRemoveEmpty(transformedDiff)
|
||||||
return transformedDiff
|
return transformedDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
//Transformation rule 1
|
// Transformation rule 1
|
||||||
// 1. if a multiline del block is followed by an ins block,
|
// 1. if a multiline del block is followed by an ins block,
|
||||||
// the first line of the ins block should be inserted at the end of the first line of the del block
|
// the first line of the ins block should be inserted at the end of the first line of the del block
|
||||||
// so the markdown will apply to the ins text as it should
|
// so the markdown will apply to the ins text as it should
|
||||||
function applyTransformationRule1(diff) {
|
function applyTransformationRuleMultilineDelThenIns (diff) {
|
||||||
let transformedDiff = []
|
let transformedDiff = []
|
||||||
|
|
||||||
const B_ADDED='added', B_REMOVED='removed', B_SAME='same'
|
const B_ADDED = 'added'
|
||||||
let previousBlockType = null
|
const B_REMOVED = 'removed'
|
||||||
|
const B_SAME = 'same'
|
||||||
|
|
||||||
|
let previousBlockType = null
|
||||||
let currentBlockType = null
|
let currentBlockType = null
|
||||||
let previousBlockWasMultiline = false
|
let previousBlockWasMultiline = false
|
||||||
let currentBlockIsMultiline = false
|
let currentBlockIsMultiline = false
|
||||||
|
|
||||||
//iterate the input tokens to create the intermediate representation
|
// iterate the input tokens to create the intermediate representation
|
||||||
diff.forEach((currentBlock) => {
|
diff.forEach((currentBlock) => {
|
||||||
|
|
||||||
previousBlockType = currentBlockType
|
previousBlockType = currentBlockType
|
||||||
previousBlockWasMultiline = currentBlockIsMultiline
|
previousBlockWasMultiline = currentBlockIsMultiline
|
||||||
currentBlockType = (currentBlock.added ? B_ADDED : (currentBlock.removed ? B_REMOVED : B_SAME))
|
currentBlockType = (currentBlock.added ? B_ADDED : (currentBlock.removed ? B_REMOVED : B_SAME))
|
||||||
currentBlockIsMultiline = isMultilineDiffBlock(currentBlock)
|
currentBlockIsMultiline = isMultilineDiffBlock(currentBlock)
|
||||||
|
|
||||||
//transform rule 1 applys when:
|
// transform rule 1 applys when:
|
||||||
// the previous block was a del and had multiple lines
|
// the previous block was a del and had multiple lines
|
||||||
// the current block is an ins
|
// the current block is an ins
|
||||||
if (previousBlockType == B_REMOVED && currentBlockType == B_ADDED && previousBlockWasMultiline) {
|
if (previousBlockType === B_REMOVED && currentBlockType === B_ADDED && previousBlockWasMultiline) {
|
||||||
|
// split the first line from the current block
|
||||||
//split the first line from the current block
|
|
||||||
let currentBlockSplit = splitMultilineDiffBlock(currentBlock)
|
let currentBlockSplit = splitMultilineDiffBlock(currentBlock)
|
||||||
|
|
||||||
//pop the previous diff entry
|
// pop the previous diff entry
|
||||||
let previousBlock = transformedDiff.pop()
|
let previousBlock = transformedDiff.pop()
|
||||||
|
|
||||||
//split the first line from the previous block
|
// split the first line from the previous block
|
||||||
let previousBlockSplit = splitMultilineDiffBlock(previousBlock)
|
let previousBlockSplit = splitMultilineDiffBlock(previousBlock)
|
||||||
|
|
||||||
|
// now add the blocks back, interleaving del and ins blocks
|
||||||
//now add the blocks back, interleaving del and ins blocks
|
for (let i = 0; i < Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
|
||||||
for (let i=0; i<Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
|
if (i < previousBlockSplit.length) {
|
||||||
if (i<previousBlockSplit.length)
|
|
||||||
transformedDiff.push(previousBlockSplit[i])
|
transformedDiff.push(previousBlockSplit[i])
|
||||||
if (i<currentBlockSplit.length)
|
}
|
||||||
transformedDiff.push(currentBlockSplit[i])
|
if (i < currentBlockSplit.length) { transformedDiff.push(currentBlockSplit[i]) }
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
// otherwise, we just add the current block to the transformed list
|
||||||
//otherwise, we just add the current block to the transformed list
|
|
||||||
transformedDiff.push(currentBlock)
|
transformedDiff.push(currentBlock)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -117,6 +117,43 @@ function applyTransformationRule1(diff) {
|
|||||||
return transformedDiff
|
return transformedDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transformation rule 2
|
||||||
|
// 2. multiline del and ins blocks should be broken up
|
||||||
|
// into a series of single line blocks
|
||||||
|
function applyTransformationRuleBreakUpDelIns (diff) {
|
||||||
|
let transformedDiff = []
|
||||||
|
|
||||||
|
const B_ADDED = 'added'
|
||||||
|
const B_REMOVED = 'removed'
|
||||||
|
const B_SAME = 'same'
|
||||||
|
let blockType = null
|
||||||
|
let blockIsMultiline = false
|
||||||
|
|
||||||
|
// iterate the input tokens to create the intermediate representation
|
||||||
|
diff.forEach((block) => {
|
||||||
|
blockType = (block.added ? B_ADDED : (block.removed ? B_REMOVED : B_SAME))
|
||||||
|
blockIsMultiline = isMultilineDiffBlock(block)
|
||||||
|
|
||||||
|
// transform rule applys when:
|
||||||
|
// the current block is an ins or del and is multiline
|
||||||
|
if ((blockType === B_REMOVED || blockType === B_ADDED) && blockIsMultiline) {
|
||||||
|
// split the first line from the current block
|
||||||
|
let blockSplit = splitMultilineDiffBlock(block)
|
||||||
|
|
||||||
|
blockSplit.forEach(blockSplitLine => transformedDiff.push(blockSplitLine))
|
||||||
|
} else {
|
||||||
|
// otherwise, we just add the current block to the transformed list
|
||||||
|
transformedDiff.push(block)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return transformedDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transformation rule number 4: remove empty blocks
|
||||||
|
function applyTransformationRuleRemoveEmpty (diff) {
|
||||||
|
return diff.filter(({value}) => value.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
// matches markdown prefixes that affect the formatting of the whole subsequent line
|
// matches markdown prefixes that affect the formatting of the whole subsequent line
|
||||||
// ^ - start of line
|
// ^ - start of line
|
||||||
@ -126,102 +163,73 @@ function applyTransformationRule1(diff) {
|
|||||||
// |([ \t]+[\*\+-]) - unordered lists
|
// |([ \t]+[\*\+-]) - unordered lists
|
||||||
// |([ \t]+[0-9]+\.) - numeric lists
|
// |([ \t]+[0-9]+\.) - numeric lists
|
||||||
// )?
|
// )?
|
||||||
// [ \t]* - trailing whitespace
|
// [ \t]+ - trailing whitespace
|
||||||
const MARKDOWN_PREFIX = /^([ \t]*\>)*(([ \t]*#*)|([ \t]*[\*\+-])|([ \t]*[\d]+\.))?[ \t]*/
|
const MARKDOWN_PREFIX = /^([ \t]*>)*(([ \t]*#*)|([ \t]*[*+\-])|([ \t]*[\d]+\.))?[ \t]+/
|
||||||
|
|
||||||
//matches strings that end with a newline followed by some whitespace
|
// matches strings that end with a newline followed by some whitespace
|
||||||
const NEWLINE_SUFFIX = /\n\s*$/
|
const NEWLINE_SUFFIX = /\n\s*$/
|
||||||
|
|
||||||
// transformation rule 2:
|
// transformation rule 3:
|
||||||
// after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
|
// after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
|
||||||
// then that prefix should be moved out of the block
|
// then that prefix should be moved out of the block
|
||||||
// also, if an ins block begins with a formatting prefix and follows immediately after a del block that follows a newline,
|
// also, if an ins block begins with a formatting prefix and follows immediately after a del block that follows a newline,
|
||||||
// the prefix should be moved out of the block _and_ an extra newline character should be added to the beginning of it
|
// the prefix should be moved out of the block _and_ an extra newline character should be added to the beginning of it
|
||||||
function applyTransformationRule2(diff) {
|
function applyTransformationRuleFormattingPrefix (diff) {
|
||||||
let transformedDiff = []
|
let transformedDiff = []
|
||||||
|
|
||||||
let isNewline = true
|
let isNewline = true
|
||||||
let newlineString = '\n'
|
let newlineString = '\n'
|
||||||
|
|
||||||
//iterate the input tokens to create the intermediate representation
|
// iterate the input tokens to create the intermediate representation
|
||||||
diff.forEach((currentBlock) => {
|
diff.forEach((currentBlock) => {
|
||||||
|
if (isNewline && (currentBlock.added || currentBlock.removed)) {
|
||||||
if (isNewline && (currentBlock.added || currentBlock.removed) ) {
|
|
||||||
let match = currentBlock.value.match(MARKDOWN_PREFIX)
|
let match = currentBlock.value.match(MARKDOWN_PREFIX)
|
||||||
if (match) {
|
if (match) {
|
||||||
let preBlock = {value:match[0]}
|
let preBlock = {value: match[0]}
|
||||||
let postBlock = {added:currentBlock.added, removed:currentBlock.removed, value:currentBlock.value.substring(match[0].length)}
|
let postBlock = {added: currentBlock.added, removed: currentBlock.removed, value: currentBlock.value.substring(match[0].length)}
|
||||||
|
|
||||||
if (currentBlock.added) {
|
if (currentBlock.added) {
|
||||||
let newlineBlock = {value: newlineString}
|
let newlineBlock = {value: newlineString}
|
||||||
transformedDiff.push(newlineBlock)
|
transformedDiff.push(newlineBlock)
|
||||||
}
|
}
|
||||||
transformedDiff.push(preBlock)
|
transformedDiff.push(preBlock)
|
||||||
transformedDiff.push(postBlock)
|
transformedDiff.push(postBlock)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
transformedDiff.push(currentBlock)
|
transformedDiff.push(currentBlock)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
transformedDiff.push(currentBlock)
|
transformedDiff.push(currentBlock)
|
||||||
isNewline = NEWLINE_SUFFIX.test(currentBlock.value)
|
isNewline = NEWLINE_SUFFIX.test(currentBlock.value)
|
||||||
if (isNewline)
|
if (isNewline) {
|
||||||
newlineString = currentBlock.value.match(NEWLINE_SUFFIX)[0]
|
newlineString = currentBlock.value.match(NEWLINE_SUFFIX)[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return transformedDiff
|
return transformedDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns true if the given diff block contains a newline element
|
||||||
|
function isMultilineDiffBlock ({value}) {
|
||||||
//returns true if the given diff block contains a newline element
|
return value.indexOf('\n') !== -1
|
||||||
function isMultilineDiffBlock({value}) {
|
|
||||||
return value.indexOf('\n') != -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns an array of diff blocks that have the same added, removed fields as the given one
|
||||||
//returns an array of diff blocks that have the same added, removed fields as the given one
|
// but with the string split by newlines
|
||||||
//but with the string split by newlines
|
// if the diff block has no newlines, an array containing only that diff will be returned
|
||||||
//if the diff block has no newlines, an array containing only that diff will be returned
|
// if the diff block has newlines, the resulting array will have a series of blocks,
|
||||||
//if the diff block has newlines, the resulting array will have a series of blocks,
|
// consisting of the block text, interleaved with newlines
|
||||||
// each of which subsequent to the first block will begin with a newline
|
// ,
|
||||||
//if the diff block begins with a newline, the returned array will begin with an empty diff
|
// each of which begin with a newline
|
||||||
function splitMultilineDiffBlock({added, removed, value}) {
|
// if the diff block begins with a newline, the returned array will begin with an empty diff
|
||||||
//find the indices of the diff block that coorespond to newlines
|
function splitMultilineDiffBlock ({added, removed, value}) {
|
||||||
const splits = indicesOf(value, c => (c=='\n') )
|
let lines = value.split('\n')
|
||||||
|
let blocks = []
|
||||||
splits.push(value.length)
|
// lines = lines.filter(line=>line.length>0)
|
||||||
|
lines.forEach((line, index) => {
|
||||||
//create a range from each index
|
blocks.push({added, removed, value: line})
|
||||||
const ranges = splits.reduce(
|
if (index < lines.length - 1) blocks.push({value: '\n'})
|
||||||
//the accumulator is a structure with the last index and the list of ranges
|
})
|
||||||
//the ranges are a {start, end} structure
|
|
||||||
({last, ranges}, i) => {
|
|
||||||
ranges = ranges.concat([{start:last, end:i}])
|
|
||||||
return {last:i, ranges}
|
|
||||||
},
|
|
||||||
//start with the zero index and an empty array
|
|
||||||
{last: 0, ranges:[]}
|
|
||||||
).ranges
|
|
||||||
|
|
||||||
|
|
||||||
//map the ranges into blocks
|
|
||||||
const blocks = ranges.map(
|
|
||||||
//each block is the same as the given original block, but with the values split at newlines
|
|
||||||
({start, end}) => ({added, removed, value:value.substring(start, end)})
|
|
||||||
)
|
|
||||||
|
|
||||||
//console.log({value, splits, ranges, blocks})
|
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
//collect all the indices of the given string that satisfy the test function
|
|
||||||
const indicesOf = (string, test) => string.split('').reduce(
|
|
||||||
//add indexes that satisfy the test function to the array
|
|
||||||
(acc, x, i) => (test(x) ? acc.concat([i]) : acc ),
|
|
||||||
//start with the empty array
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
@ -1,30 +1,37 @@
|
|||||||
var Path = require('path');
|
var Path = require('path')
|
||||||
|
|
||||||
var srcRoot = Path.join(__dirname, '..')
|
var srcRoot = Path.join(__dirname, '..')
|
||||||
|
|
||||||
//there should be some option for distribution / optimization?
|
// there should be some option for distribution / optimization?
|
||||||
var config = {
|
var config = {
|
||||||
presets: ["node6", "react"],
|
presets: ['node6', 'react'],
|
||||||
//enable source maps for non-production instances
|
// enable source maps for non-production instances
|
||||||
sourceMaps: (process.env.NODE_ENV !== "production" ? "both" : false),
|
sourceMaps: (process.env.NODE_ENV !== 'production' ? 'both' : false),
|
||||||
//highlightCode: false,
|
// highlightCode: false,
|
||||||
sourceRoot: srcRoot,
|
sourceRoot: srcRoot,
|
||||||
only: /src/
|
only: /src/
|
||||||
|
|
||||||
};
|
}
|
||||||
|
|
||||||
require('babel-core/register')(config);
|
require('babel-core/register')(config)
|
||||||
|
|
||||||
// Enable piping for non-production environments
|
var piping = require('piping')
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
if (!require("piping")({hook: true, includeModules: false})) {
|
main()
|
||||||
return;
|
|
||||||
|
function main () {
|
||||||
|
// Enable piping for non-production environments
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// piping will return false for the initial invocation
|
||||||
|
// the app will be run again in an instance managed by piping
|
||||||
|
if (!piping({hook: true, includeModules: false})) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require('./index.js')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.stack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
require('./index.js');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
|
@ -3,89 +3,85 @@ import jf from 'jsonfile'
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import uuid from 'uuid'
|
import uuid from 'uuid'
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/:id', showComparison)
|
router.get('/:id', showComparison)
|
||||||
router.post('/:id', createComparisonWithId)
|
router.post('/:id', createComparisonWithId)
|
||||||
router.post('/', createComparison)
|
router.post('/', createComparison)
|
||||||
|
|
||||||
//return the comparison given an id, if it exsits
|
// return the comparison given an id, if it exsits
|
||||||
function showComparison(req, res) {
|
function showComparison (req, res) {
|
||||||
const id = req.params.id
|
const id = req.params.id
|
||||||
return readRecord(res, id)
|
return readRecord(res, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new comparison
|
// Creates a new comparison
|
||||||
function createComparison(req, res) {
|
function createComparison (req, res) {
|
||||||
//generate a new id
|
// generate a new id
|
||||||
const id = uuid()
|
const id = uuid()
|
||||||
const {a,b} = req.body
|
const {a, b} = req.body
|
||||||
|
|
||||||
return writeRecord(res, id, {a,b,id})
|
return writeRecord(res, id, {a, b, id})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new comparison
|
// Creates a new comparison
|
||||||
function createComparisonWithId(req, res) {
|
function createComparisonWithId (req, res) {
|
||||||
//use the id provided in the req
|
// use the id provided in the req
|
||||||
const id = req.params.id
|
const id = req.params.id
|
||||||
const {a, b} = req.body
|
const {a, b} = req.body
|
||||||
|
|
||||||
return writeRecord(res, id, {a, b, id})
|
return writeRecord(res, id, {a, b, id})
|
||||||
}
|
}
|
||||||
|
|
||||||
//reads the record from the database
|
// reads the record from the database
|
||||||
function readRecord(res, id, data) {
|
function readRecord (res, id, data) {
|
||||||
//generate a filename
|
// generate a filename
|
||||||
const filename = fnData(id)
|
const filename = fnData(id)
|
||||||
|
|
||||||
//check if that file exists
|
// check if that file exists
|
||||||
fs.exists(filename, function (exists) {
|
fs.exists(filename, function (exists) {
|
||||||
//if the file does not exist, return a 404
|
// if the file does not exist, return a 404
|
||||||
if (!exists) return res.status(404).send(`Data id ${id} not found.`)
|
if (!exists) return res.status(404).send(`Data id ${id} not found.`)
|
||||||
|
|
||||||
//otherwise, read the file as JSON
|
// otherwise, read the file as JSON
|
||||||
jf.readFile(filename, function(err, data) {
|
jf.readFile(filename, function (err, data) {
|
||||||
if(err) { return handleError(res, err) }
|
if (err) { return handleError(res, err) }
|
||||||
|
|
||||||
//and return
|
// and return
|
||||||
return res.json(data)
|
return res.json(data)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//writes the record to the database, if it doesn't exist
|
// writes the record to the database, if it doesn't exist
|
||||||
function writeRecord(res, id, data) {
|
function writeRecord (res, id, data) {
|
||||||
|
// look up its filename
|
||||||
//look up its filename
|
|
||||||
var filename = fnData(id)
|
var filename = fnData(id)
|
||||||
|
|
||||||
//need to test that the file does not exist
|
// need to test that the file does not exist
|
||||||
|
|
||||||
//check if that file exists
|
// check if that file exists
|
||||||
fs.exists(filename, (exists) => {
|
fs.exists(filename, (exists) => {
|
||||||
//if the file already exists, return a 405
|
// if the file already exists, return a 405
|
||||||
if (exists) return res.status(405).send(`Data id ${id} is already in use.`)
|
if (exists) return res.status(405).send(`Data id ${id} is already in use.`)
|
||||||
|
|
||||||
|
// and write it to the filesystem
|
||||||
//and write it to the filesystem
|
|
||||||
jf.writeFile(filename, data, (err) => (
|
jf.writeFile(filename, data, (err) => (
|
||||||
err ?
|
err
|
||||||
handleError(res, err) :
|
? handleError(res, err)
|
||||||
//if successful, return the comparison object
|
// if successful, return the comparison object
|
||||||
res.status(201).json(data)
|
: res.status(201).json(data)
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
||||||
function handleError(res, err) {
|
function handleError (res, err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return res.send(500, err)
|
return res.send(500, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// returns a filename for the given comparison
|
// returns a filename for the given comparison
|
||||||
function fnData (id) {
|
function fnData (id) {
|
||||||
return `./data/${id}.json`
|
return `./data/${id}.json`
|
||||||
|
@ -7,84 +7,77 @@ import fetch from 'isomorphic-fetch'
|
|||||||
|
|
||||||
import comparisonRouter from './comparison'
|
import comparisonRouter from './comparison'
|
||||||
|
|
||||||
|
|
||||||
import * as reducers from '../common/reducers'
|
import * as reducers from '../common/reducers'
|
||||||
|
|
||||||
import {Status, StatusError} from '../common/constants'
|
import {Status, StatusError} from '../common/constants'
|
||||||
|
|
||||||
import render from './render'
|
import render from './render'
|
||||||
|
|
||||||
const PORT = 8080
|
// set use port 8080 for dev, 80 for production
|
||||||
|
const PORT = (process.env.NODE_ENV !== 'production' ? 8080 : 80)
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
//serve the dist static files at /dist
|
// serve the dist static files at /dist
|
||||||
app.use('/dist', express.static(path.join(__dirname, '..', '..', 'dist')))
|
app.use('/dist', express.static(path.join(__dirname, '..', '..', 'dist')))
|
||||||
|
|
||||||
//serve the comparison api at /api/compare
|
// serve the comparison api at /api/compare
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
app.use('/api/compare', comparisonRouter);
|
app.use('/api/compare', comparisonRouter)
|
||||||
|
|
||||||
|
// the following routes are for server-side rendering of the app
|
||||||
|
// we should render the comparison directly from the server
|
||||||
//the following routes are for server-side rendering of the app
|
// this loading logic could be moved into ../common/actions because it is isomorphic
|
||||||
//we should render the comparison directly from the server
|
|
||||||
//this loading logic could be moved into ../common/actions because it is isomorphic
|
|
||||||
app.route('/:comparisonId')
|
app.route('/:comparisonId')
|
||||||
.get((req, res) => {
|
.get((req, res) => {
|
||||||
|
|
||||||
|
|
||||||
const store = createSessionStore()
|
const store = createSessionStore()
|
||||||
const endpointUri = `http://localhost:${PORT}/api/compare/${req.params.comparisonId}`
|
const endpointUri = `http://localhost:${PORT}/api/compare/${req.params.comparisonId}`
|
||||||
|
|
||||||
//fetch the comparison
|
// fetch the comparison
|
||||||
fetch(endpointUri)
|
fetch(endpointUri)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok)
|
if (response.ok) {
|
||||||
return response.json()
|
return response.json()
|
||||||
else {
|
} else {
|
||||||
response.text().then( () => {
|
response.text().then(() => {
|
||||||
const error = {message:`${response.status}: ${response.statusText}`}
|
const error = {message: `${response.status}: ${response.statusText}`}
|
||||||
initAndRenderError(error, store, req, res)
|
initAndRenderError(error, store, req, res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then( ({a,b}) => {
|
.then(({a, b}) => {
|
||||||
initAndRenderComparison({a,b}, store, req, res)
|
initAndRenderComparison({a, b}, store, req, res)
|
||||||
})
|
})
|
||||||
.catch( error => {
|
.catch(error => {
|
||||||
initAndRenderError(error, store, req, res)
|
initAndRenderError(error, store, req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
app.route('/')
|
app.route('/')
|
||||||
.get((req, res) => {
|
.get((req, res) => {
|
||||||
render(createSessionStore(), req, res)
|
render(createSessionStore(), req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
app.listen(PORT, function () {
|
app.listen(PORT, function () {
|
||||||
console.log('Server listening on port 8080.')
|
console.log(`Server listening on port ${PORT}.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// creates the session store
|
||||||
//creates the session store
|
function createSessionStore () {
|
||||||
function createSessionStore() {
|
// create the redux store
|
||||||
//create the redux store
|
|
||||||
return Redux.createStore(
|
return Redux.createStore(
|
||||||
Redux.combineReducers(reducers)
|
Redux.combineReducers(reducers)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAndRenderComparison({a,b}, store, req, res) {
|
function initAndRenderComparison ({a, b}, store, req, res) {
|
||||||
store.dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: a})
|
store.dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: a})
|
||||||
store.dispatch({type: 'UPDATE_FINAL_INPUT', data: b})
|
store.dispatch({type: 'UPDATE_FINAL_INPUT', data: b})
|
||||||
store.dispatch({type: 'STATUS_SET', data: Status.CLEAN})
|
store.dispatch({type: 'STATUS_SET', data: Status.CLEAN})
|
||||||
render(store, req, res)
|
render(store, req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAndRenderError(error, store, req, res) {
|
function initAndRenderError (error, store, req, res) {
|
||||||
store.dispatch({type: 'STATUS_SET', data: Status.EMPTY})
|
store.dispatch({type: 'STATUS_SET', data: Status.EMPTY})
|
||||||
store.dispatch({type: 'STATUS_SET_ERROR', data: StatusError.LOAD_ERROR, error})
|
store.dispatch({type: 'STATUS_SET_ERROR', data: StatusError.LOAD_ERROR, error})
|
||||||
render(store, req, res)
|
render(store, req, res)
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,11 @@ import { match, RouterContext } from 'react-router'
|
|||||||
|
|
||||||
import routes from '../common/routes.js'
|
import routes from '../common/routes.js'
|
||||||
|
|
||||||
|
export default function render (store, req, res) {
|
||||||
export default function render(store, req, res) {
|
|
||||||
// Send the rendered page back to the client
|
// Send the rendered page back to the client
|
||||||
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
|
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
res.status(500).send(renderError('Routing Error:', error.message))
|
res.status(500).send(errorTemplate('Routing Error:', error.message))
|
||||||
} else if (redirectLocation) {
|
} else if (redirectLocation) {
|
||||||
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
|
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
|
||||||
} else if (renderProps) {
|
} else if (renderProps) {
|
||||||
@ -18,7 +17,7 @@ export default function render(store, req, res) {
|
|||||||
try {
|
try {
|
||||||
const html = renderToString(
|
const html = renderToString(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<RouterContext {...renderProps} />
|
<RouterContext {...renderProps} />
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,12 +25,10 @@ export default function render(store, req, res) {
|
|||||||
const initialState = store.getState()
|
const initialState = store.getState()
|
||||||
// and send
|
// and send
|
||||||
res.status(200).send(appTemplate(html, initialState))
|
res.status(200).send(appTemplate(html, initialState))
|
||||||
}
|
} catch (ex) {
|
||||||
catch(ex) {
|
console.log('Render Exception:', ex)
|
||||||
console.log("Render Exception:",ex)
|
|
||||||
res.status(500).send(errorTemplate('Render Exception', ex.message, ex))
|
res.status(500).send(errorTemplate('Render Exception', ex.message, ex))
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send(errorTemplate('Not found', `${req.url} not found.`))
|
res.status(404).send(errorTemplate('Not found', `${req.url} not found.`))
|
||||||
}
|
}
|
||||||
@ -39,7 +36,7 @@ export default function render(store, req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pageTemplate = (body) => {
|
const pageTemplate = (body) => {
|
||||||
return `
|
return `
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@ -54,7 +51,6 @@ const pageTemplate = (body) => {
|
|||||||
<link rel="icon" type="image/png" sizes="96x96" href="dist/favicon-96x96.png">
|
<link rel="icon" type="image/png" sizes="96x96" href="dist/favicon-96x96.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="dist/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="dist/favicon-16x16.png">
|
||||||
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${body}
|
${body}
|
||||||
@ -63,25 +59,23 @@ const pageTemplate = (body) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorTemplate(title, message, exception) {
|
function errorTemplate (title, message, exception) {
|
||||||
return pageTemplate(`
|
return pageTemplate(`
|
||||||
<h1>${title}</h1>
|
<h1>${title}</h1>
|
||||||
<p>${message}</p>
|
<p>${message}</p>
|
||||||
${exception ?
|
${exception
|
||||||
`<pre>${exception.toString()}</pre>`:
|
? `<pre>${exception.toString()}</pre>`
|
||||||
``
|
: ``
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appTemplate (html, initialState) {
|
||||||
|
return pageTemplate(`
|
||||||
function appTemplate(html, initialState) {
|
|
||||||
return pageTemplate(`
|
|
||||||
<div id="root">${html}</div>
|
<div id="root">${html}</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
|
window.__INITIAL_STATE__ = "${encodeURI(JSON.stringify(initialState, null, 2))}"
|
||||||
</script>
|
</script>
|
||||||
<!-- <script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script> -->
|
<!-- <script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script> -->
|
||||||
<script type="text/javascript" src="dist/browser-bundle.js"></script>
|
<script type="text/javascript" src="dist/browser-bundle.js"></script>
|
||||||
|
@ -1,74 +1,85 @@
|
|||||||
/*eslint-env node, mocha */
|
/* eslint-env node, mocha */
|
||||||
/*global expect */
|
/* global expect */
|
||||||
/*eslint no-console: 0*/
|
/* eslint no-console: 0 */
|
||||||
'use strict';
|
'use strict'
|
||||||
|
|
||||||
import chai from 'chai'
|
import chai from 'chai'
|
||||||
|
|
||||||
import {markdownDiff, diffToString} from '../src/common/util/dubdiff'
|
import {markdownDiff, diffToString} from '../src/common/util/dubdiff'
|
||||||
|
|
||||||
let diff = (a,b) => diffToString(markdownDiff(a,b))
|
let diff = (a, b) => diffToString(markdownDiff(a, b))
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect // eslint-disable-line no-unused-vars
|
||||||
|
|
||||||
describe('dubdiff', () => {
|
describe('dubdiff', () => {
|
||||||
let db;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
});
|
})
|
||||||
|
|
||||||
it('plaintext diffs consecutive words', ()=>{
|
it('plaintext diffs consecutive words', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'This is a smlb sentnce with no errors.',
|
'This is a smlb sentnce with no errors.',
|
||||||
'This is a simple sentence with no errors.'
|
'This is a simple sentence with no errors.'
|
||||||
)).to.equal('This is a [-smlb sentnce -]{+simple sentence +}with no errors.')
|
)).to.equal('This is a [-smlb sentnce -]{+simple sentence +}with no errors.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('plaintext diffs with word deletion', ()=>{
|
it('plaintext diffs with word deletion', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'Gonna delete a word.',
|
'Gonna delete a word.',
|
||||||
'Gonna delete word.'
|
'Gonna delete word.'
|
||||||
)).to.equal('Gonna delete [-a -]word.')
|
)).to.equal('Gonna delete [-a -]word.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('plaintext diffs with word insertion', ()=>{
|
it('plaintext diffs with word insertion', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'Gonna delete word.',
|
'Gonna delete word.',
|
||||||
'Gonna delete a word.'
|
'Gonna delete a word.'
|
||||||
)).to.equal('Gonna delete {+a +}word.')
|
)).to.equal('Gonna delete {+a +}word.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reorganizes insertions after multiline deletions', ()=>{
|
it('reorganizes insertions after multiline deletions', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
`# Title
|
`# Title
|
||||||
other`,
|
other`,
|
||||||
`# Subtitle`
|
`# Subtitle`
|
||||||
)).to.equal('# [-Title-]{+Subtitle+}[-\nother-]')
|
)).to.equal('# [-Title-]{+Subtitle+}\n[-other-]')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pulls prefixes out of ins or del blocks after newline', () => {
|
it('pulls prefixes out of ins or del blocks after newline', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'# Title\n > hello',
|
'# Title\n > hello',
|
||||||
'# Title\n - goodbye'
|
'# Title\n - goodbye'
|
||||||
)).to.equal('# Title\n > [-hello-]\n - {+goodbye+}')
|
)).to.equal('# Title\n > [-hello-]\n - {+goodbye+}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('respects bold and italic boundaries', () => {
|
it('respects bold and italic boundaries', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'This *word* **isn\'t** changed.',
|
'This *word* **isn\'t** changed.',
|
||||||
'This *other one* **is** changed.'
|
'This *other one* **is** changed.'
|
||||||
)).to.equal('This *[-word-]{+other one+}* **[-isn\'t-]{+is+}** changed.')
|
)).to.equal('This *[-word-]{+other one+}* **[-isn\'t-]{+is+}** changed.')
|
||||||
})
|
})
|
||||||
it('respects link boundaries in link text', () => {
|
it('respects link boundaries in link text', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'This [link](https://somewhere.com) is the same.',
|
'This [link](https://somewhere.com) is the same.',
|
||||||
'This [target](https://somewhere.com) changed.'
|
'This [target](https://somewhere.com) changed.'
|
||||||
)).to.equal('This [[-link-]{+target+}](https://somewhere.com) [-is the same-]{+changed+}.')
|
)).to.equal('This [[-link-]{+target+}](https://somewhere.com) [-is the same-]{+changed+}.')
|
||||||
})
|
})
|
||||||
it('respects link boundaries in link target', () => {
|
it('respects link boundaries in link target', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'This [link](https://somewhere.com) is the same.',
|
'This [link](https://somewhere.com) is the same.',
|
||||||
'This [link](https://somewhere.org) changed.'
|
'This [link](https://somewhere.org) changed.'
|
||||||
)).to.equal('This [link](https://somewhere.[-com-]{+org+}) [-is the same-]{+changed+}.')
|
)).to.equal('This [link](https://somewhere.[-com-]{+org+}) [-is the same-]{+changed+}.')
|
||||||
})
|
})
|
||||||
})
|
it('deletes a title', () => {
|
||||||
|
expect(diff(
|
||||||
|
'Hello\n# Title 1\n# Title 2',
|
||||||
|
'Hello\n# Title 2'
|
||||||
|
)).to.equal('Hello\n# Title [-1-]\n# [-Title -]2')
|
||||||
|
})
|
||||||
|
it('deletes a more different title', () => {
|
||||||
|
expect(diff(
|
||||||
|
'Hello\n# Filbert\n# Title 2',
|
||||||
|
'Hello\n# Title 2'
|
||||||
|
)).to.equal('Hello\n# [-Filbert-]\n# Title 2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
@ -1,42 +1,41 @@
|
|||||||
/*eslint-env node, mocha */
|
/* eslint-env node, mocha */
|
||||||
/*global expect */
|
/* global expect */
|
||||||
/*eslint no-console: 0*/
|
/* eslint no-console: 0 */
|
||||||
'use strict';
|
'use strict'
|
||||||
|
|
||||||
import chai from 'chai'
|
import chai from 'chai'
|
||||||
|
|
||||||
import {plaintextDiff, diffToString} from '../src/common/util/dubdiff'
|
import {plaintextDiff, diffToString} from '../src/common/util/dubdiff'
|
||||||
|
|
||||||
let diff = (a,b) => diffToString(plaintextDiff(a,b))
|
let diff = (a, b) => diffToString(plaintextDiff(a, b))
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect // eslint-disable-line no-unused-vars
|
||||||
|
|
||||||
describe('dubdiff', () => {
|
describe('dubdiff', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
});
|
})
|
||||||
|
|
||||||
it('diffs single words', ()=>{
|
it('diffs single words', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'This is a smlb sentence.',
|
'This is a smlb sentence.',
|
||||||
'This is a simple sentence.'
|
'This is a simple sentence.'
|
||||||
)).to.equal('This is a [-smlb -]{+simple +}sentence.')
|
)).to.equal('This is a [-smlb -]{+simple +}sentence.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('diffs consecutive words', ()=>{
|
it('diffs consecutive words', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'This is a smlb sentnce with no errors.',
|
'This is a smlb sentnce with no errors.',
|
||||||
'This is a simple sentence with no errors.'
|
'This is a simple sentence with no errors.'
|
||||||
)).to.equal('This is a [-smlb sentnce -]{+simple sentence +}with no errors.')
|
)).to.equal('This is a [-smlb sentnce -]{+simple sentence +}with no errors.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('diffs with word deletion', ()=>{
|
it('diffs with word deletion', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'Gonna delete a word.',
|
'Gonna delete a word.',
|
||||||
'Gonna delete word.'
|
'Gonna delete word.'
|
||||||
)).to.equal('Gonna delete [-a -]word.')
|
)).to.equal('Gonna delete [-a -]word.')
|
||||||
})
|
})
|
||||||
it('diffs with word insertion', ()=>{
|
it('diffs with word insertion', () => {
|
||||||
expect(diff(
|
expect(diff(
|
||||||
'Gonna add word.',
|
'Gonna add word.',
|
||||||
'Gonna add a word.'
|
'Gonna add a word.'
|
||||||
@ -66,4 +65,4 @@ describe('dubdiff', () => {
|
|||||||
'Hello, world.'
|
'Hello, world.'
|
||||||
)).to.equal('Hello{+, +}world.')
|
)).to.equal('Hello{+, +}world.')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -13,10 +13,10 @@ let config = {
|
|||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
query: {
|
query: {
|
||||||
presets: ['es2015-native-modules', 'react'],
|
presets: ['es2015-native-modules', 'react'],
|
||||||
compact: "true"
|
compact: 'true'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ test: /\.json$/, loader: "json-loader" },
|
{ test: /\.json$/, loader: 'json-loader' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
@ -24,13 +24,12 @@ let config = {
|
|||||||
net: 'empty',
|
net: 'empty',
|
||||||
tls: 'empty'
|
tls: 'empty'
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV == "production") {
|
|
||||||
config.devtool = "cheap-module-source-map"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
config.devtool = "eval"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config;
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
config.devtool = 'cheap-module-source-map'
|
||||||
|
} else {
|
||||||
|
config.devtool = 'eval'
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config
|
||||||
|
Loading…
Reference in New Issue
Block a user