format to standardjs code style with linting

This commit is contained in:
Adam Brown 2017-01-17 20:41:53 -05:00
parent 8c5ac6ade7
commit 368d19dd21
26 changed files with 518 additions and 582 deletions

View File

@ -12,6 +12,8 @@
"serve": "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",
"lint": "standard --verbose | snazzy",
"lint:fix": "standard --fix --verbose | snazzy",
"test": "mocha --watch --compilers js:babel-register"
},
"author": "",
@ -55,6 +57,8 @@
"json-loader": "^0.5.4",
"mocha": "^3.2.0",
"piping": "^1.0.0-rc.4",
"snazzy": "^6.0.0",
"standard": "^8.6.0",
"webpack": "^2.1.0-beta.27"
}
}

View File

@ -1,75 +1,71 @@
import React from 'react'
import {connect} from 'react-redux'
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.
* Local storage is read during the componentDidMount lifecycle method.
* Local storage is written during the componentWillReceiveProps lifecycle method.
*/
//an app-specific name for the localStorage state
const stateName = "dubdiff_state"
// an app-specific name for the localStorage state
const stateName = 'dubdiff_state'
//return a new object with the given keys, each assigned to the cooresponding value
//from the given object
const copyKeys = (obj, keys) => keys.reduce((acc, p)=>{acc[p]=obj[p]; return acc}, {})
// return a new object with the given keys, each assigned to the cooresponding value
// from the given object
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) {
if (localStorage.getItem(stateName)) {
const localState = JSON.parse(localStorage.getItem(stateName))
if (window.localStorage.getItem(stateName)) {
const localState = JSON.parse(window.localStorage.getItem(stateName))
return copyKeys(localState, keys)
}
else
} else {
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) {
let toSave = copyKeys(state, keys)
localStorage.setItem(stateName, JSON.stringify(toSave))
window.localStorage.setItem(stateName, JSON.stringify(toSave))
}
const mapStateToProps = (state) => ({
input: state.input,
//the loading/empty/clean state
input: state.input
// the loading/empty/clean state
})
const mapDispatchToProps = dispatch => ({
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text)),
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text))
})
class LocalStorage extends React.Component {
//load the state from the local storage
componentDidMount() {
//only if the status is EMPTY
// load the state from the local storage
componentDidMount () {
// only if the status is EMPTY
/*
if (this.props.input.original=='' && this.props.input.final == '') {
const localState = getLocalState(['input'])
if (localState.input && 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)
}
*/
}
//save the state to local storage
componentWillReceiveProps(nextProps) {
// save the state to local storage
componentWillReceiveProps (nextProps) {
setLocalState(nextProps, ['input'])
}
render () {
render () {
return this.props.children
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LocalStorage)

View File

@ -5,26 +5,23 @@ import * as Redux from 'redux'
import {Provider} from 'react-redux'
//import createBrowserHistory from 'history/lib/createBrowserHistory'
import {Router, Route, IndexRoute, Redirect, browserHistory } from 'react-router'
// import createBrowserHistory from 'history/lib/createBrowserHistory'
import {Router, browserHistory} from 'react-router'
import thunk from 'redux-thunk'
import * as reducers from '../common/reducers'
import routes from '../common/routes'
import * as Actions from '../common/actions'
import LocalStorage from './LocalStorage'
//initial state is rehydrated from the server
// initial state is rehydrated from the server
const initialState = JSON.parse(decodeURI(window.__INITIAL_STATE__))
//create the redux store
//initial state is retrieved from localStore
// create the redux store
// initial state is retrieved from localStore
const store = Redux.createStore(
Redux.combineReducers(reducers),
Redux.combineReducers(reducers),
initialState,
Redux.compose(
Redux.applyMiddleware(thunk),
@ -32,14 +29,12 @@ const store = Redux.createStore(
)
)
function render() {
ReactDOM.render(
function render () {
ReactDOM.render(
<Provider store={store}>
<LocalStorage >
<Router history={browserHistory}>
{routes}
{routes}
</Router>
</LocalStorage>
</Provider>
@ -47,4 +42,4 @@ function render() {
}
render()

View File

@ -3,84 +3,81 @@ import uuid from 'uuid/v4'
import {browserHistory} from 'react-router'
import {Status, StatusError} from './constants'
//All state transitions in the app happen in these methods
//this includes redux state changes, asyncronous data requests, and browser location changes
// All state transitions in the app happen in these methods
// this includes redux state changes, asyncronous data requests, and browser location changes
export const updateOriginalInput = (text) =>
export const updateOriginalInput = (text) =>
(dispatch, getState) => {
dispatch({type: 'UPDATE_ORIGINAL_INPUT', data:text})
if (getState().input.original.length>0)
dispatch({type: 'STATUS_SET', data:Status.DIRTY})
else
dispatch({type: 'STATUS_SET', data:Status.EMPTY})
dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: text})
if (getState().input.original.length > 0) {
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
} else {
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
}
}
export const updateFinalInput = (text) =>
export const updateFinalInput = (text) =>
(dispatch, getState) => {
dispatch({ type: 'UPDATE_FINAL_INPUT', data:text})
if (getState().input.final.length>0)
dispatch({type: 'STATUS_SET', data:Status.DIRTY})
else
dispatch({type: 'STATUS_SET', data:Status.EMPTY})
dispatch({type: 'UPDATE_FINAL_INPUT', data: text})
if (getState().input.final.length > 0) {
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
} else {
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
}
}
export const clearInput = () =>
(dispatch) => {
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 setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT'})
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL'})
export const showFinal = () => ({ type: 'SHOW_FINAL'})
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE'})
export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT' })
export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT' })
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL' })
export const showFinal = () => ({ type: 'SHOW_FINAL' })
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE' })
//if the input is dirty, saves it to the server
//creates a new uuid for the same,
//then changes the browser location to a comparison view with that id
export const compare = () =>
// if the input is dirty, saves it to the server
// creates a new uuid for the same,
// then changes the browser location to a comparison view with that id
export const compare = () =>
(dispatch, getState) => {
//!!! 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 clean, the compare should not save and keep using the same id
//! !! 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 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())
//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}`
browserHistory.replace(comparePath)
}
//clear the input and return to the edit page
export const reset = () =>
// clear the input and return to the edit page
export const reset = () =>
(dispatch, getState) => {
dispatch(clearInput())
browserHistory.push('/')
}
//switch to the edit view
export const edit = () =>
// switch to the edit view
export const edit = () =>
(dispatch, getState) => {
browserHistory.push('/')
}
//saves the current input fields to the server
//creates and returns a new id for the comparison
//should this method ensure that the initial state is valid? ('DIRTY')
// saves the current input fields to the server
// creates and returns a new id for the comparison
// should this method ensure that the initial state is valid? ('DIRTY')
export const save = () =>
(dispatch, getState) => {
//generate an id
// generate an id
const id = uuid()
//set waiting state
dispatch( {type: 'STATUS_SET', data:Status.SAVING})
// set waiting state
dispatch({type: 'STATUS_SET', data: Status.SAVING})
const endpointUri = `/api/compare/${id}`
const fetchOptions = {
@ -90,32 +87,31 @@ export const save = () =>
b: getState().input.final
}),
headers: {
"Content-Type": "application/json"
},
'Content-Type': 'application/json'
}
}
//dispatch post request
// dispatch post request
fetch(endpointUri, fetchOptions)
.then(response => {
if (response.ok)
if (response.ok) {
dispatch({type: 'STATUS_SET', data: Status.CLEAN})
else {
response.text().then( (responseText) => {
const error = {message:`${response.status}: ${responseText}`}
} else {
response.text().then((responseText) => {
const error = {message: `${response.status}: ${responseText}`}
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, 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_ERROR', data: StatusError.SAVE_ERROR, error})
})
//return the id after the request has been sent
return id;
// return the id after the request has been sent
return id
}
/*
@ -130,7 +126,6 @@ const load = (id) =>
method: 'GET'
}
//dispatch post request
fetch(endpointUri, fetchOptions)
.then(response => response.json())
@ -152,4 +147,4 @@ export const loadIfNeeded = (id) =>
(dispatch, getState) => {
if
}
*/
*/

View File

@ -1,9 +1,8 @@
import React from 'react'
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 Header from './Header'
@ -15,19 +14,17 @@ import ShowMarkdown from './ShowMarkdown'
const mapStateToProps = (state) => ({
isMarkdownFormat: Selectors.isMarkdownFormat(state),
isShowOriginal: Selectors.isShowOriginal(state),
isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state),
safeInput: Selectors.safeInput(state),
safeInput: Selectors.safeInput(state),
diff: Selectors.diff(state)
})
const mapDispatchToProps = dispatch => ({
//loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
// loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
})
class Compare extends React.Component {
/*
componentDidMount() {
@ -35,39 +32,39 @@ class Compare extends React.Component {
}
*/
render() {
render () {
return (
<div>
<Header/>
<Header />
<Segment basic padded>
<Grid stackable columns={2}>
<Grid.Column width="3">
<CompareControls/>
<Grid.Column width='3'>
<CompareControls />
</Grid.Column>
<Grid.Column width="13">
<Grid.Column width='13'>
<Segment>
{
(!this.props.isMarkdownFormat && this.props.isShowDifference) ?
<ShowPlaintext diff={this.props.diff}>{this.props.diff}</ShowPlaintext>:
(this.props.isMarkdownFormat && this.props.isShowDifference) ?
<ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>:
(!this.props.isMarkdownFormat && !this.props.isShowDifference) ?
<ShowPlaintext
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
/> :
(this.props.isMarkdownFormat && !this.props.isShowDifference) ?
<ShowMarkdown
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
/> :
null
(!this.props.isMarkdownFormat && this.props.isShowDifference)
? <ShowPlaintext diff={this.props.diff}>{this.props.diff}</ShowPlaintext>
: (this.props.isMarkdownFormat && this.props.isShowDifference)
? <ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>
: (!this.props.isMarkdownFormat && !this.props.isShowDifference)
? <ShowPlaintext
text={this.props.isShowOriginal ? this.props.safeInput.original : this.props.safeInput.final}
/>
: (this.props.isMarkdownFormat && !this.props.isShowDifference)
? <ShowMarkdown
text={this.props.isShowOriginal ? this.props.safeInput.original : this.props.safeInput.final}
/>
: null
}
</Segment>
</Grid.Column>
</Grid>
</Segment>
<Footer/>
<Footer />
</div>
)
}
@ -75,10 +72,9 @@ class Compare extends React.Component {
export default connect(mapStateToProps, mapDispatchToProps)(Compare)
/* <div ng-if="isMarkdownFormat">
<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 ng-show="isShowWdiff" class="col-md-10 col-sm-12 content-well">
@ -101,4 +97,4 @@ export default connect(mapStateToProps, mapDispatchToProps)(Compare)
<div ng-bind-html="after" class="content-pre after"></div>
</div>
</div>
*/
*/

View File

@ -1,6 +1,5 @@
import React from 'react'
import {connect} from 'react-redux'
import {Link} from 'react-router'
import {Button, Icon, Segment} from 'semantic-ui-react'
@ -11,13 +10,12 @@ const mapStateToProps = (state) => ({
isMarkdownFormat: Selectors.isMarkdownFormat(state),
isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state),
isShowDifference: Selectors.isShowDifference(state)
})
const mapDispatchToProps = dispatch => ({
onSetPlaintextFormat: () => dispatch(Actions.setPlaintextFormat()),
onSetMarkdownFormat: () => dispatch(Actions.setMarkdownFormat()),
onSetMarkdownFormat: () => dispatch(Actions.setMarkdownFormat()),
onShowOriginal: () => dispatch(Actions.showOriginal()),
onShowFinal: () => dispatch(Actions.showFinal()),
onShowDifference: () => dispatch(Actions.showDifference()),
@ -26,19 +24,19 @@ const mapDispatchToProps = dispatch => ({
class CompareControls extends React.Component {
onClickMarkdownFormat() {
if (this.props.isMarkdownFormat)
onClickMarkdownFormat () {
if (this.props.isMarkdownFormat) {
this.props.onSetPlaintextFormat()
else
} else {
this.props.onSetMarkdownFormat()
}
}
render() {
render () {
return (
<Segment.Group>
<Segment>
<Button fluid onClick={this.props.onEdit}>Edit</Button>
<Button fluid onClick={this.props.onEdit}>Edit</Button>
</Segment>
<Segment >
@ -48,8 +46,8 @@ class CompareControls extends React.Component {
</Segment>
<Segment >
<Button fluid active={this.props.isMarkdownFormat} type="submit" onClick={this.onClickMarkdownFormat.bind(this)}>
{this.props.isMarkdownFormat ? <Icon name="checkmark"/> : <span/>}
<Button fluid active={this.props.isMarkdownFormat} type='submit' onClick={this.onClickMarkdownFormat.bind(this)}>
{this.props.isMarkdownFormat ? <Icon name='checkmark' /> : <span />}
&nbsp;As Markdown
</Button>
</Segment>

View File

@ -3,9 +3,9 @@ import React from 'react'
import {Segment} from 'semantic-ui-react'
const Footer = (props) => (
<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>
<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>
</Segment>
)
export default Footer
export default Footer

View File

@ -1,7 +1,7 @@
import React from 'react'
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 * as Actions from '../actions'
@ -10,27 +10,25 @@ import SaveStatus from './SaveStatus'
const mapStateToProps = (state) => ({
})
const mapDispatchToProps = dispatch => ({
onReset: () => { console.log(Actions.reset()); dispatch(Actions.reset())},
onReset: () => { console.log(Actions.reset()); dispatch(Actions.reset()) }
})
const SiteHeader = (props) => (
<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 >
<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>
)
export default connect(mapStateToProps, mapDispatchToProps)(SiteHeader)

View File

@ -13,59 +13,50 @@ import MainControls from './MainControls'
const mapStateToProps = (state) => ({
input: state.input,
safeInput: Selectors.safeInput(state),
safeInput: Selectors.safeInput(state)
})
const mapDispatchToProps = dispatch => ({
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text)),
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text))
})
class Main extends React.Component {
constructor() {
super()
}
render () {
render () {
return (
<div>
<Header/>
<Header />
<Segment basic padded>
<Grid stackable columns={3}>
<Grid.Column width="3">
<MainControls/>
<Grid.Column width='3'>
<MainControls />
</Grid.Column>
<Grid.Column width="6">
<Grid.Column width='6'>
<Form>
<Form.Field>
<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>
</Grid.Column>
<Grid.Column width="6">
<Form>
<Grid.Column width='6'>
<Form>
<Form.Field>
<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>
</Grid.Column>
</Grid>
</Segment>
<Footer/>
<Footer />
</div>
)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Main)

View File

@ -7,31 +7,30 @@ import * as Actions from '../actions'
import * as Selectors from '../selectors'
const mapStateToProps = (state) => ({
format: state.format,
format: state.format,
isMarkdownFormat: Selectors.isMarkdownFormat(state),
saveStatus: state.saveStatus
})
const mapDispatchToProps = dispatch => ({
onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()),
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
//returns an id for the record to be saved
onCompare: () => dispatch(Actions.compare())
// returns an id for the record to be saved
onCompare: () => dispatch(Actions.compare())
})
class MainControls extends React.Component {
onClickMarkdownFormat() {
if (this.props.isMarkdownFormat)
onClickMarkdownFormat () {
if (this.props.isMarkdownFormat) {
this.props.onSetPlaintextFormat()
else
} else {
this.props.onSetMarkdownFormat()
}
}
render() {
render () {
return (
<Segment.Group>
<Segment >
@ -39,8 +38,8 @@ class MainControls extends React.Component {
</Segment>
<Segment >
<Button fluid active={this.props.isMarkdownFormat} type="submit" onClick={this.onClickMarkdownFormat.bind(this)}>
{this.props.isMarkdownFormat ? <Icon name="checkmark"/> : <span/>}
<Button fluid active={this.props.isMarkdownFormat} type='submit' onClick={this.onClickMarkdownFormat.bind(this)}>
{this.props.isMarkdownFormat ? <Icon name='checkmark' /> : <span />}
&nbsp;As Markdown
</Button>
</Segment>
@ -52,4 +51,4 @@ class MainControls extends React.Component {
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> */

View File

@ -1,8 +1,7 @@
import React from 'react'
import {connect} from 'react-redux'
import { Message, Icon, Button} from 'semantic-ui-react'
import { browserHistory} from 'react-router'
import {Message, Icon, Button} from 'semantic-ui-react'
import * as Actions from '../actions'
import {Status, StatusError} from '../constants'
@ -11,54 +10,56 @@ const mapStateToProps = (state) => ({
status: state.status
})
const mapDispatchToProps = dispatch => ({
onSave: () => dispatch(Actions.save()),
onReset: () => dispatch(Actions.reset())
})
const SaveStatus = (props) => {
if (props.status.type == Status.SAVING) return (
<Message size='tiny' floating compact icon>
<Icon name='circle notched' loading />
<Message.Content>
<Message.Header>Saving diff</Message.Header>
</Message.Content>
</Message>
)
if (props.status.type == Status.LOADING) return (
<Message size='tiny' floating compact icon>
<Icon name='circle notched' loading />
<Message.Content>
<Message.Header>Loading diff</Message.Header>
</Message.Content>
</Message>
)
else if (props.status.hasError && props.status.errorType == StatusError.SAVE_ERROR) return (
<Message size='tiny' floating compact icon>
<Icon name='exclamation' />
<Message.Content>
<Message.Header>Error saving diff</Message.Header>
{props.status.error.message}
<br/>
<Button onClick={props.onSave}>Retry</Button>
</Message.Content>
</Message>
)
else if (props.status.hasError && props.status.errorType == StatusError.LOAD_ERROR) return (
<Message size='tiny' floating compact icon>
<Icon name='exclamation' />
<Message.Content>
<Message.Header>Error loading diff</Message.Header>
{props.status.error.message}
<br/>
<Button onClick={props.onReset}>New Diff</Button>
</Message.Content>
</Message>
)
else return ( <div></div> )
if (props.status.type === Status.SAVING) {
return (
<Message size='tiny' floating compact icon>
<Icon name='circle notched' loading />
<Message.Content>
<Message.Header>Saving diff</Message.Header>
</Message.Content>
</Message>
)
}
if (props.status.type === Status.LOADING) {
return (
<Message size='tiny' floating compact icon>
<Icon name='circle notched' loading />
<Message.Content>
<Message.Header>Loading diff</Message.Header>
</Message.Content>
</Message>
)
} else if (props.status.hasError && props.status.errorType === StatusError.SAVE_ERROR) {
return (
<Message size='tiny' floating compact icon>
<Icon name='exclamation' />
<Message.Content>
<Message.Header>Error saving diff</Message.Header>
{props.status.error.message}
<br />
<Button onClick={props.onSave}>Retry</Button>
</Message.Content>
</Message>
)
} else if (props.status.hasError && props.status.errorType === StatusError.LOAD_ERROR) {
return (
<Message size='tiny' floating compact icon>
<Icon name='exclamation' />
<Message.Content>
<Message.Header>Error loading diff</Message.Header>
{props.status.error.message}
<br />
<Button onClick={props.onReset}>New Diff</Button>
</Message.Content>
</Message>
)
} else return (<div />)
}
export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus)

View File

@ -1,18 +1,17 @@
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) => {
return <div>
{
props.text ?
markdownCompiler(props.text) :
props.diff ?
markdownCompiler(diffToHtml(props.diff)) :
null
{
props.text
? markdownCompiler(props.text)
: props.diff
? markdownCompiler(diffToHtml(props.diff))
: null
}
</div>
}

View File

@ -1,14 +1,13 @@
import React from 'react'
const ShowPlaintext = (props) => {
return <div>
<pre style={{whiteSpace:'pre-wrap'}}>
{props.text ?
props.text:
props.diff ?
diffToPre(props.diff) :
null
<pre style={{whiteSpace: 'pre-wrap'}}>
{props.text
? props.text
: props.diff
? diffToPre(props.diff)
: null
}
</pre>
</div>
@ -16,10 +15,12 @@ const ShowPlaintext = (props) => {
export default ShowPlaintext
function diffToPre(diff) {
function diffToPre (diff) {
return diff.map((part, index) => (
part.added ? <ins key={index}>{part.value}</ins> :
part.removed ? <del key={index}>{part.value}</del> :
<span key={index}>{part.value}</span>
part.added
? <ins key={index}>{part.value}</ins>
: part.removed
? <del key={index}>{part.value}</del>
: <span key={index}>{part.value}</span>
))
}

View File

@ -4,9 +4,9 @@ export const Format = {
}
export const Show = {
ORIGINAL:'ORIGINAL',
FINAL:'FINAL',
DIFFERENCE:'DIFFERENCE'
ORIGINAL: 'ORIGINAL',
FINAL: 'FINAL',
DIFFERENCE: 'DIFFERENCE'
}
export const Status = {
@ -21,4 +21,4 @@ export const Status = {
export const StatusError = {
LOAD_ERROR: 'LOAD_ERROR',
SAVE_ERROR: 'SAVE_ERROR'
}
}

View File

@ -1,16 +1,15 @@
import {Format, Show, Status, StatusError} from './constants'
export function input (state, action ) {
export function input (state, action) {
switch (action.type) {
case 'UPDATE_ORIGINAL_INPUT':
return Object.assign({}, state, {original:action.data})
return Object.assign({}, state, {original: action.data})
case 'UPDATE_FINAL_INPUT':
return Object.assign({}, state, {final:action.data})
return Object.assign({}, state, {final: action.data})
case 'CLEAR_INPUT':
return {original:'', final:''}
return {original: '', final: ''}
default:
return state || {original:'', final:''}
return state || {original: '', final: ''}
}
}
@ -23,8 +22,7 @@ export function format (state, action) {
default:
return state || Format.PLAINTEXT
}
}
}
export function show (state, action) {
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) {
//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 isValidError = (type) => StatusError[type] == type
// 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 isValidError = (type) => StatusError[type] === type
//the error is cleared when status changes
if (action.type == 'STATUS_SET' && isValidStatus(action.data))
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))
return Object.assign({}, state, {error: action.error, hasError: true, errorType:action.data})
else
return state || {type:Status.EMPTY, hasError: false, error:null}
// the error is cleared when status changes
if (action.type === 'STATUS_SET' && isValidStatus(action.data)) {
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)) {
return Object.assign({}, state, {error: action.error, hasError: true, errorType: action.data})
} else {
return state || {type: Status.EMPTY, hasError: false, error: null}
}
}

View File

@ -1,12 +1,12 @@
import {Route, IndexRout, Redirect } from 'react-router'
import React from 'react';
import {Route} from 'react-router'
import React from 'react'
import Main from './components/Main'
import Compare from './components/Compare'
var routes = [
<Route key="root" path="/" component={Main}/>,
<Route key="compare" path="/:compareId" component={Compare}/>
<Route key='root' path='/' component={Main} />,
<Route key='compare' path='/:compareId' component={Compare} />
]

View File

@ -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 React from 'react'
import {Format, Show} from './constants'
import * as Dubdiff from './util/dubdiff'
const input = (state) => state.input
const format = (state) => state.format
const show = (state) => state.show
const show = (state) => state.show
export const safeInput = createSelector(
[input],
(input) => {
//!!! sanitize the input here and return
//! !! sanitize the input here and return
return input
}
)
@ -25,38 +23,37 @@ export const safeInput = createSelector(
export const isMarkdownFormat = createSelector(
[format],
(format) => {
return format == Format.MARKDOWN
return format === Format.MARKDOWN
}
)
const isShow = (type) => createSelector(
[show],
(show) => {
return show == type
return show === type
}
)
export const isShowOriginal = isShow(Show.ORIGINAL)
export const isShowFinal = isShow(Show.FINAL)
export const isShowDifference= isShow(Show.DIFFERENCE)
export const isShowDifference = isShow(Show.DIFFERENCE)
export const diff = createSelector(
[format, safeInput],
(format, safeInput) => {
if (format==Format.PLAINTEXT)
if (format === Format.PLAINTEXT) {
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
else if (format==Format.MARKDOWN)
} else if (format === Format.MARKDOWN) {
return Dubdiff.markdownDiff(safeInput.original, safeInput.final)
}
}
)
/*
html diff
---
diffHtml(parentOriginal, parentFinal) {
create stringOriginal, stringFinal consisting of
create stringOriginal, stringFinal consisting of
}
*/
*/

View File

@ -6,44 +6,42 @@ import {Diff} from 'diff'
// but is preserved and included in the output.
const TOKEN_BOUNDARYS = /([\s,.:])/
class EditorsDiff extends Diff {
constructor (tokenBoundaries=TOKEN_BOUNDARYS) {
constructor (tokenBoundaries = TOKEN_BOUNDARYS) {
super()
this.tokenBoundaries = tokenBoundaries
}
equals (left, right) {
return (
left.string == right.string
left.string === right.string
)
}
//splits the input string into a series of word and punctuation tokens
//each token is associated with an optional trailing array of spaces
// splits the input string into a series of word and punctuation tokens
// each token is associated with an optional trailing array of spaces
tokenize (value) {
let tokens = value.split(this.tokenBoundaries)
let annotatedTokens = []
tokens.forEach( token => {
let annotatedTokens = []
tokens.forEach(token => {
if (isSpace(token)) {
if (annotatedTokens.length == 0)
annotatedTokens.push({string:'', whitespace:[]})
if (annotatedTokens.length === 0) {
annotatedTokens.push({string: '', whitespace: []})
}
let last = annotatedTokens[annotatedTokens.length-1]
let last = annotatedTokens[annotatedTokens.length - 1]
last.whitespace.push(token)
}
else {
annotatedTokens.push({string:token, whitespace:[]})
} else {
annotatedTokens.push({string: token, whitespace: []})
}
})
//this final empty token is necessary for the jsdiff diffing engine to work properly
annotatedTokens.push({string:'', whitespace:[]})
// this final empty token is necessary for the jsdiff diffing engine to work properly
annotatedTokens.push({string: '', whitespace: []})
return annotatedTokens
}
join(annotatedTokens) {
join (annotatedTokens) {
let tokens = []
annotatedTokens.forEach(annotatedToken => {
tokens.push(annotatedToken.string)
@ -53,12 +51,9 @@ class EditorsDiff extends Diff {
})
return tokens.join('')
}
}
}
export default EditorsDiff
const isSpace = str => /[ ]+/.test(str)
const isNewline = str => /[\n]+/.test(str)
const isNewline = str => /[\n]+/.test(str)

View File

@ -1,119 +1,115 @@
import * as JsDiff from 'diff'
import EditorsDiff from './EditorsDiff'
let plaintextDiffer = new EditorsDiff()
let markdownDiffer = new EditorsDiff(/([\s,.:]|[*\[\]\(\)])/)
let markdownDiffer = new EditorsDiff(/([\s,.:]|[*\[\]()])/)
//returns a comparison of the texts as plaintext
export function plaintextDiff(original, final) {
let diff = plaintextDiffer.diff(original, final)
return diff
// returns a comparison of the texts as plaintext
export function plaintextDiff (original, final) {
let diff = plaintextDiffer.diff(original, final)
return diff
}
//returns a comparison of the texts as markdown
export function markdownDiff(original, final) {
let diff = markdownDiffer.diff(original, final)
diff = rewriteMarkdownDiff(diff)
// returns a comparison of the texts as markdown
export function markdownDiff (original, final) {
let diff = markdownDiffer.diff(original, final)
diff = rewriteMarkdownDiff(diff)
return diff
return diff
}
// returns a string version of the diff, with "{+ ... +}" and "[- ... -]"
// 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}) => {
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
if (Array.isArray(value))
string = value.join('')
return start+string+end
return start + string + end
}).join('')
}
export function diffToHtml(diff) {
return diffToString(diff, {added:{start:'<ins>', end:'</ins>'}, removed:{start:'<del>', end:'</del>'}, same:{start:'', end:''}})
export function diffToHtml (diff) {
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
// documents were also valid markdown.
// 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
// so the markdown will apply to the ins text as it should
// 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
//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
// 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
// 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
function rewriteMarkdownDiff(diff) {
//apply transformation rules
function rewriteMarkdownDiff (diff) {
// apply transformation rules
let transformedDiff = diff
transformedDiff= applyTransformationRuleMultilineDelThenIns(transformedDiff)
transformedDiff= applyTransformationRuleBreakUpDelIns(transformedDiff)
transformedDiff= applyTransformationRuleFormattingPrefix(transformedDiff)
transformedDiff= applyTransformationRuleRemoveEmpty(transformedDiff)
transformedDiff = applyTransformationRuleMultilineDelThenIns(transformedDiff)
transformedDiff = applyTransformationRuleBreakUpDelIns(transformedDiff)
transformedDiff = applyTransformationRuleFormattingPrefix(transformedDiff)
transformedDiff = applyTransformationRuleRemoveEmpty(transformedDiff)
return transformedDiff
}
//Transformation rule 1
// 1. if a multiline del block is followed by an ins block,
// Transformation rule 1
// 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
// so the markdown will apply to the ins text as it should
function applyTransformationRuleMultilineDelThenIns(diff) {
function applyTransformationRuleMultilineDelThenIns (diff) {
let transformedDiff = []
const B_ADDED='added', B_REMOVED='removed', B_SAME='same'
let previousBlockType = null
const B_ADDED = 'added'
const B_REMOVED = 'removed'
const B_SAME = 'same'
let previousBlockType = null
let currentBlockType = null
let previousBlockWasMultiline = 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) => {
previousBlockType = currentBlockType
previousBlockWasMultiline = currentBlockIsMultiline
currentBlockType = (currentBlock.added ? B_ADDED : (currentBlock.removed ? B_REMOVED : B_SAME))
currentBlockIsMultiline = isMultilineDiffBlock(currentBlock)
//transform rule 1 applys when:
// transform rule 1 applys when:
// the previous block was a del and had multiple lines
// the current block is an ins
if (previousBlockType == B_REMOVED && currentBlockType == B_ADDED && previousBlockWasMultiline) {
//split the first line from the current block
if (previousBlockType === B_REMOVED && currentBlockType === B_ADDED && previousBlockWasMultiline) {
// split the first line from the current block
let currentBlockSplit = splitMultilineDiffBlock(currentBlock)
//pop the previous diff entry
// pop the previous diff entry
let previousBlock = transformedDiff.pop()
//split the first line from the previous block
// split the first line from the previous block
let previousBlockSplit = splitMultilineDiffBlock(previousBlock)
//now add the blocks back, interleaving del and ins blocks
for (let i=0; i<Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
if (i<previousBlockSplit.length)
// now add the blocks back, interleaving del and ins blocks
for (let i = 0; i < Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
if (i < previousBlockSplit.length) {
transformedDiff.push(previousBlockSplit[i])
if (i<currentBlockSplit.length)
transformedDiff.push(currentBlockSplit[i])
}
if (i < currentBlockSplit.length) { transformedDiff.push(currentBlockSplit[i]) }
}
}
else {
//otherwise, we just add the current block to the transformed list
} else {
// otherwise, we just add the current block to the transformed list
transformedDiff.push(currentBlock)
}
})
@ -121,33 +117,32 @@ function applyTransformationRuleMultilineDelThenIns(diff) {
return transformedDiff
}
//Transformation rule 2
// 2. multiline del and ins blocks should be broken up
// Transformation rule 2
// 2. multiline del and ins blocks should be broken up
// into a series of single line blocks
function applyTransformationRuleBreakUpDelIns(diff) {
function applyTransformationRuleBreakUpDelIns (diff) {
let transformedDiff = []
const B_ADDED='added', B_REMOVED='removed', B_SAME='same'
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
// 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:
// 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
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
} else {
// otherwise, we just add the current block to the transformed list
transformedDiff.push(block)
}
})
@ -155,11 +150,9 @@ function applyTransformationRuleBreakUpDelIns(diff) {
return transformedDiff
}
// Transformation rule number 4: remove empty blocks
function applyTransformationRuleRemoveEmpty(diff) {
return diff.filter(({value}) => value.length>0)
function applyTransformationRuleRemoveEmpty (diff) {
return diff.filter(({value}) => value.length > 0)
}
// matches markdown prefixes that affect the formatting of the whole subsequent line
@ -171,9 +164,9 @@ function applyTransformationRuleRemoveEmpty(diff) {
// |([ \t]+[0-9]+\.) - numeric lists
// )?
// [ \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*$/
// transformation rule 3:
@ -181,70 +174,62 @@ const NEWLINE_SUFFIX = /\n\s*$/
// 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,
// the prefix should be moved out of the block _and_ an extra newline character should be added to the beginning of it
function applyTransformationRuleFormattingPrefix(diff) {
function applyTransformationRuleFormattingPrefix (diff) {
let transformedDiff = []
let isNewline = true
let newlineString = '\n'
//iterate the input tokens to create the intermediate representation
// iterate the input tokens to create the intermediate representation
diff.forEach((currentBlock) => {
if (isNewline && (currentBlock.added || currentBlock.removed) ) {
if (isNewline && (currentBlock.added || currentBlock.removed)) {
let match = currentBlock.value.match(MARKDOWN_PREFIX)
if (match) {
let preBlock = {value:match[0]}
let postBlock = {added:currentBlock.added, removed:currentBlock.removed, value:currentBlock.value.substring(match[0].length)}
let preBlock = {value: match[0]}
let postBlock = {added: currentBlock.added, removed: currentBlock.removed, value: currentBlock.value.substring(match[0].length)}
if (currentBlock.added) {
let newlineBlock = {value: newlineString}
transformedDiff.push(newlineBlock)
}
transformedDiff.push(preBlock)
transformedDiff.push(postBlock)
}
else {
} else {
transformedDiff.push(currentBlock)
}
}
else {
} else {
transformedDiff.push(currentBlock)
isNewline = NEWLINE_SUFFIX.test(currentBlock.value)
if (isNewline)
if (isNewline) {
newlineString = currentBlock.value.match(NEWLINE_SUFFIX)[0]
}
}
})
return transformedDiff
}
//returns true if the given diff block contains a newline element
function isMultilineDiffBlock({value}) {
return value.indexOf('\n') != -1
// returns true if the given diff block contains a newline element
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
//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 newlines, the resulting array will have a series of blocks,
// returns an array of diff blocks that have the same added, removed fields as the given one
// 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 newlines, the resulting array will have a series of blocks,
// consisting of the block text, interleaved with newlines
// ,
// ,
// each of which begin with a newline
//if the diff block begins with a newline, the returned array will begin with an empty diff
function splitMultilineDiffBlock({added, removed, value}) {
// if the diff block begins with a newline, the returned array will begin with an empty diff
function splitMultilineDiffBlock ({added, removed, value}) {
let lines = value.split('\n')
let blocks = []
// lines = lines.filter(line=>line.length>0)
lines.forEach((line, index) => {
blocks.push({added, removed, value:line})
if (index<lines.length-1) blocks.push({value:'\n'})
} )
console.log(lines)
console.log(blocks)
blocks.push({added, removed, value: line})
if (index < lines.length - 1) blocks.push({value: '\n'})
})
return blocks
}
}

View File

@ -1,30 +1,37 @@
var Path = require('path');
var Path = require('path')
var srcRoot = Path.join(__dirname, '..')
//there should be some option for distribution / optimization?
var config = {
presets: ["node6", "react"],
//enable source maps for non-production instances
sourceMaps: (process.env.NODE_ENV !== "production" ? "both" : false),
//highlightCode: false,
// there should be some option for distribution / optimization?
var config = {
presets: ['node6', 'react'],
// enable source maps for non-production instances
sourceMaps: (process.env.NODE_ENV !== 'production' ? 'both' : false),
// highlightCode: false,
sourceRoot: srcRoot,
only: /src/
};
}
require('babel-core/register')(config);
require('babel-core/register')(config)
// Enable piping for non-production environments
if (process.env.NODE_ENV !== "production") {
if (!require("piping")({hook: true, includeModules: false})) {
return;
var piping = require('piping')
main()
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);
}

View File

@ -3,89 +3,85 @@ import jf from 'jsonfile'
import fs from 'fs'
import uuid from 'uuid'
const router = express.Router()
router.get('/:id', showComparison)
router.post('/:id', createComparisonWithId)
router.post('/', createComparison)
//return the comparison given an id, if it exsits
function showComparison(req, res) {
// return the comparison given an id, if it exsits
function showComparison (req, res) {
const id = req.params.id
return readRecord(res, id)
}
// Creates a new comparison
function createComparison(req, res) {
//generate a new id
const id = uuid()
const {a,b} = req.body
function createComparison (req, res) {
// generate a new id
const id = uuid()
const {a, b} = req.body
return writeRecord(res, id, {a,b,id})
return writeRecord(res, id, {a, b, id})
}
// Creates a new comparison
function createComparisonWithId(req, res) {
//use the id provided in the req
function createComparisonWithId (req, res) {
// use the id provided in the req
const id = req.params.id
const {a, b} = req.body
return writeRecord(res, id, {a, b, id})
}
//reads the record from the database
function readRecord(res, id, data) {
//generate a filename
// reads the record from the database
function readRecord (res, id, data) {
// generate a filename
const filename = fnData(id)
//check if that file exists
// check if that file exists
fs.exists(filename, function (exists) {
//if the file does not exist, return a 404
if (!exists) return res.status(404).send(`Data id ${id} not found.`)
// if the file does not exist, return a 404
if (!exists) return res.status(404).send(`Data id ${id} not found.`)
//otherwise, read the file as JSON
jf.readFile(filename, function(err, data) {
if(err) { return handleError(res, err) }
// otherwise, read the file as JSON
jf.readFile(filename, function (err, data) {
if (err) { return handleError(res, err) }
//and return
// and return
return res.json(data)
})
})
}
//writes the record to the database, if it doesn't exist
function writeRecord(res, id, data) {
//look up its filename
// writes the record to the database, if it doesn't exist
function writeRecord (res, id, data) {
// look up its filename
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) => {
//if the file already exists, return a 405
if (exists) return res.status(405).send(`Data id ${id} is already in use.`)
// if the file already exists, return a 405
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) => (
err ?
handleError(res, err) :
//if successful, return the comparison object
res.status(201).json(data)
err
? handleError(res, err)
// if successful, return the comparison object
: res.status(201).json(data)
))
})
}
module.exports = router
function handleError(res, err) {
function handleError (res, err) {
console.log(err)
return res.send(500, err)
}
// returns a filename for the given comparison
function fnData (id) {
return `./data/${id}.json`

View File

@ -7,85 +7,77 @@ import fetch from 'isomorphic-fetch'
import comparisonRouter from './comparison'
import * as reducers from '../common/reducers'
import {Status, StatusError} from '../common/constants'
import render from './render'
//set use port 8080 for dev, 80 for production
const PORT = (process.env.NODE_ENV !== "production" ? 8080 : 80)
// set use port 8080 for dev, 80 for production
const PORT = (process.env.NODE_ENV !== 'production' ? 8080 : 80)
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')))
//serve the comparison api at /api/compare
// serve the comparison api at /api/compare
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
//this loading logic could be moved into ../common/actions because it is isomorphic
// the following routes are for server-side rendering of the app
// 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')
.get((req, res) => {
const store = createSessionStore()
const endpointUri = `http://localhost:${PORT}/api/compare/${req.params.comparisonId}`
//fetch the comparison
// fetch the comparison
fetch(endpointUri)
.then(response => {
if (response.ok)
if (response.ok) {
return response.json()
else {
response.text().then( () => {
const error = {message:`${response.status}: ${response.statusText}`}
} else {
response.text().then(() => {
const error = {message: `${response.status}: ${response.statusText}`}
initAndRenderError(error, store, req, res)
})
}
})
.then( ({a,b}) => {
initAndRenderComparison({a,b}, store, req, res)
.then(({a, b}) => {
initAndRenderComparison({a, b}, store, req, res)
})
.catch( error => {
.catch(error => {
initAndRenderError(error, store, req, res)
})
})
app.route('/')
.get((req, res) => {
render(createSessionStore(), req, res)
})
app.listen(PORT, function () {
console.log(`Server listening on port ${PORT}.`)
})
//creates the session store
function createSessionStore() {
//create the redux store
// creates the session store
function createSessionStore () {
// create the redux store
return Redux.createStore(
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_FINAL_INPUT', data: b})
store.dispatch({type: 'STATUS_SET', data: Status.CLEAN})
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_ERROR', data: StatusError.LOAD_ERROR, error})
render(store, req, res)
}
}

View File

@ -5,12 +5,11 @@ import { match, RouterContext } from 'react-router'
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
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(renderError('Routing Error:', error.message))
res.status(500).send(errorTemplate('Routing Error:', error.message))
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
@ -18,7 +17,7 @@ export default function render(store, req, res) {
try {
const html = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
<RouterContext {...renderProps} />
</Provider>
)
@ -26,12 +25,10 @@ export default function render(store, req, res) {
const initialState = store.getState()
// and send
res.status(200).send(appTemplate(html, initialState))
}
catch(ex) {
console.log("Render Exception:",ex)
} catch (ex) {
console.log('Render Exception:', ex)
res.status(500).send(errorTemplate('Render Exception', ex.message, ex))
}
} else {
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) => {
return `
return `
<!doctype html>
<html>
<head>
@ -62,25 +59,23 @@ const pageTemplate = (body) => {
`
}
function errorTemplate(title, message, exception) {
function errorTemplate (title, message, exception) {
return pageTemplate(`
<h1>${title}</h1>
<p>${message}</p>
${exception ?
`<pre>${exception.toString()}</pre>`:
``
${exception
? `<pre>${exception.toString()}</pre>`
: ``
}
`)
}
function appTemplate(html, initialState) {
return pageTemplate(`
function appTemplate (html, initialState) {
return pageTemplate(`
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = "${encodeURI(JSON.stringify(initialState,null,2))}"
window.__INITIAL_STATE__ = "${encodeURI(JSON.stringify(initialState, null, 2))}"
</script>
<!-- <script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script> -->
<script type="text/javascript" src="dist/browser-bundle.js"></script>

View File

@ -1,44 +1,42 @@
/*eslint-env node, mocha */
/*global expect */
/*eslint no-console: 0*/
'use strict';
/* eslint-env node, mocha */
/* global expect */
/* eslint no-console: 0 */
'use strict'
import chai from 'chai'
import {markdownDiff, diffToString, diffToHtml} 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', () => {
let db;
beforeEach(() => {
});
})
it('plaintext diffs consecutive words', ()=>{
it('plaintext diffs consecutive words', () => {
expect(diff(
'This is a smlb sentnce with no errors.',
'This is a 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(
'Gonna delete a word.',
'Gonna delete word.'
)).to.equal('Gonna delete [-a -]word.')
})
it('plaintext diffs with word insertion', ()=>{
it('plaintext diffs with word insertion', () => {
expect(diff(
'Gonna delete word.',
'Gonna delete a word.'
)).to.equal('Gonna delete {+a +}word.')
})
it('reorganizes insertions after multiline deletions', ()=>{
it('reorganizes insertions after multiline deletions', () => {
expect(diff(
`# Title
other`,
@ -46,42 +44,42 @@ other`,
)).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(
'# Title\n > hello',
'# Title\n - goodbye'
)).to.equal('# Title\n > [-hello-]\n - {+goodbye+}')
})
it('respects bold and italic boundaries', () => {
it('respects bold and italic boundaries', () => {
expect(diff(
'This *word* **isn\'t** changed.',
'This *other one* **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(
'This [link](https://somewhere.com) is the same.',
'This [target](https://somewhere.com) 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(
'This [link](https://somewhere.com) is the same.',
'This [link](https://somewhere.org) changed.'
)).to.equal('This [link](https://somewhere.[-com-]{+org+}) [-is the same-]{+changed+}.')
})
it('deletes a title' , () => {
})
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',)
)).to.equal('Hello\n# Title [-1-]\n# [-Title -]2')
})
it('deletes a more different title' , () => {
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',)
)).to.equal('Hello\n# [-Filbert-]\n# Title 2')
})
})

View File

@ -1,42 +1,41 @@
/*eslint-env node, mocha */
/*global expect */
/*eslint no-console: 0*/
'use strict';
/* eslint-env node, mocha */
/* global expect */
/* eslint no-console: 0 */
'use strict'
import chai from 'chai'
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', () => {
beforeEach(() => {
});
})
it('diffs single words', ()=>{
it('diffs single words', () => {
expect(diff(
'This is a smlb sentence.',
'This is a simple sentence.'
)).to.equal('This is a [-smlb -]{+simple +}sentence.')
})
it('diffs consecutive words', ()=>{
it('diffs consecutive words', () => {
expect(diff(
'This is a smlb sentnce with no errors.',
'This is a 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(
'Gonna delete a word.',
'Gonna delete word.'
)).to.equal('Gonna delete [-a -]word.')
})
it('diffs with word insertion', ()=>{
it('diffs with word insertion', () => {
expect(diff(
'Gonna add word.',
'Gonna add a word.'
@ -66,4 +65,4 @@ describe('dubdiff', () => {
'Hello, world.'
)).to.equal('Hello{+, +}world.')
})
})
})

View File

@ -13,10 +13,10 @@ let config = {
loader: 'babel-loader',
query: {
presets: ['es2015-native-modules', 'react'],
compact: "true"
compact: 'true'
}
},
{ test: /\.json$/, loader: "json-loader" },
{ test: /\.json$/, loader: 'json-loader' }
]
},
node: {
@ -24,13 +24,12 @@ let config = {
net: '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