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": "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": "",
@ -55,6 +57,8 @@
"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"
} }
} }

View File

@ -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)

View File

@ -5,26 +5,23 @@ 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 LocalStorage from './LocalStorage' 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__)) const initialState = JSON.parse(decodeURI(window.__INITIAL_STATE__))
//create the redux store // create the redux store
//initial state is retrieved from localStore // 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,14 +29,12 @@ const store = Redux.createStore(
) )
) )
function render () {
ReactDOM.render(
function 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>
@ -47,4 +42,4 @@ function render() {
} }
render() render()

View File

@ -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
} }
*/ */

View File

@ -1,9 +1,8 @@
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 Header from './Header' import Header from './Header'
@ -15,19 +14,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 +32,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 +72,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 +97,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>
*/ */

View File

@ -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 />}
&nbsp;As Markdown &nbsp;As Markdown
</Button> </Button>
</Segment> </Segment>

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 />}
&nbsp;As Markdown &nbsp;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> */

View File

@ -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)

View File

@ -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>
} }

View File

@ -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>
)) ))
} }

View File

@ -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'
} }

View File

@ -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}
}
} }

View File

@ -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} />
] ]

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 { 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,38 +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) => {
if (format==Format.PLAINTEXT) if (format === Format.PLAINTEXT) {
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final) return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
else if (format==Format.MARKDOWN) } else if (format === Format.MARKDOWN) {
return Dubdiff.markdownDiff(safeInput.original, safeInput.final) return Dubdiff.markdownDiff(safeInput.original, safeInput.final)
}
} }
) )
/* /*
html diff html diff
--- ---
diffHtml(parentOriginal, parentFinal) { 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. // 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,12 +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) const isNewline = str => /[\n]+/.test(str)

View File

@ -1,119 +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. multiline ins and del blocks should be broken up into a series of single line blocks // 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) // 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= applyTransformationRuleMultilineDelThenIns(transformedDiff) transformedDiff = applyTransformationRuleMultilineDelThenIns(transformedDiff)
transformedDiff= applyTransformationRuleBreakUpDelIns(transformedDiff) transformedDiff = applyTransformationRuleBreakUpDelIns(transformedDiff)
transformedDiff= applyTransformationRuleFormattingPrefix(transformedDiff) transformedDiff = applyTransformationRuleFormattingPrefix(transformedDiff)
transformedDiff= applyTransformationRuleRemoveEmpty(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 applyTransformationRuleMultilineDelThenIns(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)
} }
}) })
@ -121,33 +117,32 @@ function applyTransformationRuleMultilineDelThenIns(diff) {
return transformedDiff return transformedDiff
} }
//Transformation rule 2 // Transformation rule 2
// 2. multiline del and ins blocks should be broken up // 2. multiline del and ins blocks should be broken up
// into a series of single line blocks // into a series of single line blocks
function applyTransformationRuleBreakUpDelIns(diff) { function applyTransformationRuleBreakUpDelIns (diff) {
let transformedDiff = [] 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 blockType = null
let blockIsMultiline = false let blockIsMultiline = false
//iterate the input tokens to create the intermediate representation // iterate the input tokens to create the intermediate representation
diff.forEach((block) => { diff.forEach((block) => {
blockType = (block.added ? B_ADDED : (block.removed ? B_REMOVED : B_SAME)) blockType = (block.added ? B_ADDED : (block.removed ? B_REMOVED : B_SAME))
blockIsMultiline = isMultilineDiffBlock(block) blockIsMultiline = isMultilineDiffBlock(block)
//transform rule applys when: // transform rule applys when:
// the current block is an ins or del and is multiline // the current block is an ins or del and is multiline
if ((blockType == B_REMOVED || blockType == B_ADDED) && blockIsMultiline) { if ((blockType === B_REMOVED || blockType === B_ADDED) && blockIsMultiline) {
// split the first line from the current block
//split the first line from the current block
let blockSplit = splitMultilineDiffBlock(block) let blockSplit = splitMultilineDiffBlock(block)
blockSplit.forEach(blockSplitLine => transformedDiff.push(blockSplitLine)) blockSplit.forEach(blockSplitLine => transformedDiff.push(blockSplitLine))
} } 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(block) transformedDiff.push(block)
} }
}) })
@ -155,11 +150,9 @@ function applyTransformationRuleBreakUpDelIns(diff) {
return transformedDiff return transformedDiff
} }
// Transformation rule number 4: remove empty blocks // Transformation rule number 4: remove empty blocks
function applyTransformationRuleRemoveEmpty(diff) { function applyTransformationRuleRemoveEmpty (diff) {
return diff.filter(({value}) => value.length > 0)
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
@ -171,9 +164,9 @@ function applyTransformationRuleRemoveEmpty(diff) {
// |([ \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 3: // transformation rule 3:
@ -181,70 +174,62 @@ const NEWLINE_SUFFIX = /\n\s*$/
// 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 applyTransformationRuleFormattingPrefix(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 // consisting of the block text, interleaved with newlines
// , // ,
// each of which begin with a newline // each of which begin with a newline
//if the diff block begins with a newline, the returned array will begin with an empty diff // if the diff block begins with a newline, the returned array will begin with an empty diff
function splitMultilineDiffBlock({added, removed, value}) { function splitMultilineDiffBlock ({added, removed, value}) {
let lines = value.split('\n') let lines = value.split('\n')
let blocks = [] let blocks = []
// lines = lines.filter(line=>line.length>0) // lines = lines.filter(line=>line.length>0)
lines.forEach((line, index) => { lines.forEach((line, index) => {
blocks.push({added, removed, value:line}) blocks.push({added, removed, value: line})
if (index<lines.length-1) blocks.push({value:'\n'}) if (index < lines.length - 1) blocks.push({value: '\n'})
} ) })
console.log(lines)
console.log(blocks)
return blocks return blocks
} }

View File

@ -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);
}

View File

@ -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`

View File

@ -7,85 +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'
//set use port 8080 for dev, 80 for production // set use port 8080 for dev, 80 for production
const PORT = (process.env.NODE_ENV !== "production" ? 8080 : 80) 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 ${PORT}.`) 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)
} }

View File

@ -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>
@ -62,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__ = "${encodeURI(JSON.stringify(initialState,null,2))}" 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>

View File

@ -1,44 +1,42 @@
/*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, 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', () => { 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`,
@ -46,42 +44,42 @@ other`,
)).to.equal('# [-Title-]{+Subtitle+}\n[-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( 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' , () => { it('deletes a title', () => {
expect(diff( expect(diff(
'Hello\n# Title 1\n# Title 2', 'Hello\n# Title 1\n# Title 2',
'Hello\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( expect(diff(
'Hello\n# Filbert\n# Title 2', 'Hello\n# Filbert\n# Title 2',
'Hello\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 */ /* 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.')
}) })
}) })

View File

@ -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