Compare commits

..

71 Commits

Author SHA1 Message Date
Mario Voigt
e1a0c0f422 FIx in readme 2022-05-18 23:04:27 +02:00
Adam Brown
7fdaf8e3ab respond to browser hash to show markdown/plaintext view 2017-02-05 15:33:55 -05:00
Adam Brown
c633afa184 respond to browser hash to show markdown/plaintext view 2017-02-05 13:36:10 -05:00
Adam Brown
368d19dd21 format to standardjs code style with linting 2017-01-17 20:41:53 -05:00
Adam Brown
8c5ac6ade7 debug markdown diff output edge cases 2017-01-05 17:18:03 -05:00
Adam Brown
70a22ad5ff encode initial state json for more secure and reliable inclusion in rendered page 2017-01-04 16:15:09 +00:00
Adam Brown
bd7161a727 Merge branch 'master' of https://github.com/adamarthurryan/dubdiff 2016-12-29 13:13:18 -05:00
Adam Brown
f521e78bd2 minor cleanup 2016-12-29 13:13:11 -05:00
Adam Brown
ed7ad72c2a merge resolve dependency conflict 2016-12-29 18:11:19 +00:00
Adam Brown
cc515695a8 fix minor: diff colors, readme, port logging 2016-12-29 13:10:32 -05:00
Adam Brown
136e9c1c8e add no copy mode to build:prod 2016-12-28 22:46:09 +00:00
Adam Brown
a2c2407d3c switch port for production server 2016-12-28 17:20:07 -05:00
Adam Brown
ae5d02b288 Merge version-2 branch to master 2016-12-28 17:07:21 -05:00
Adam Brown
966bdae3e1 reset prepatory to merge version 2 2016-12-28 17:07:10 -05:00
Adam Brown
f15069f42d minor fix readme 2016-11-04 10:46:45 -04:00
Adam Brown
2d5d72772d minor fix readme 2016-11-04 10:46:03 -04:00
Adam Brown
5a55902b5b cleanup old docker cruft 2016-11-03 19:16:54 -04:00
Adam Brown
dbc4bac6a2 update readme 2016-11-03 18:52:09 -04:00
Adam Brown
30609b17da update server port and set new dubdiff method 2016-11-03 22:28:51 +00:00
Adam Brown
ab1229e8da changes to diff algo 2016-05-24 10:56:13 -04:00
Adam Brown
3326eaccb7 minor fix, before diff engine refactor 2016-05-11 00:02:42 -04:00
Adam Brown
c764eb5b45 Update README.md 2015-09-17 16:42:22 -04:00
Adam Brown
be5c011349 Update README.md 2015-09-17 16:42:13 -04:00
Adam Brown
aa8f43c733 Update README.md 2015-09-17 16:41:50 -04:00
Adam Brown
c49cf68c74 Update README.md 2015-06-03 16:49:02 -04:00
Adam Brown
d2fdf8b170 Update README.md 2015-06-03 16:48:25 -04:00
Adam Brown
26f3a4d97f Merge branch 'master' of https://github.com/adamarthurryan/dubdiff 2015-06-03 16:10:39 -04:00
Adam Brown
0515da6468 add note about swap memory on digitalocean 2015-06-03 16:10:20 -04:00
Adam Brown
f81f54a610 production docker container will now restart on failure indefinitely 2015-05-29 13:44:16 -04:00
Adam Brown
573b6b32e2 fix path to wdiff binary 2015-05-26 11:29:41 -04:00
Adam Brown
b54dcf0c2c Merge branch 'master' of https://github.com/adamarthurryan/dubdiff 2015-05-26 10:41:49 -04:00
Adam Brown
638ec426e5 add option to display as plain text 2015-05-26 10:41:06 -04:00
Adam Brown
7bc937606d finalize deployment 2015-04-24 01:57:54 -04:00
Adam Brown
ba4c8820ba change github url 2015-04-18 10:08:18 -04:00
Adam Brown
4fe73b91bc move docker launch scripts to docker folder 2015-04-18 10:05:32 -04:00
Adam Brown
9dc35f2079 remove docker files 2015-04-17 23:17:12 -04:00
Adam Brown
73f0b5f4b3 fix wdiff markdown parse error 2015-04-18 03:12:00 +00:00
Adam Brown
24167d5b45 switch from mongodb-backed data to filesystem-backed 2015-04-17 19:53:16 -04:00
Adam Brown
de45c9b411 remove passport dependencies, user and document cruft 2015-04-17 19:17:19 -04:00
Adam Brown
9a7dc3c138 add docker definition and launch scripts 2015-04-17 13:22:55 -04:00
Adam Brown
3bf6223c92 Add 'docker/' from commit '697f105a275dab12b3cc0200a25b067d1f263e8c'
git-subtree-dir: docker
git-subtree-mainline: f27d91a803
git-subtree-split: 697f105a27
2015-04-17 13:00:37 -04:00
Adam Brown
f27d91a803 prepare for production deployment 2015-04-17 12:59:48 -04:00
Adam Brown
697f105a27 Update README.md 2015-04-16 22:37:29 -04:00
Adam Brown
98e5b186f1 Merge pull request #8 from adamarthurryan/simple-interface
Simple interface
2015-04-13 23:10:12 -04:00
Adam Brown
e2114ae55c deleted cruft 2015-04-14 03:09:28 +00:00
Adam Brown
b63474b3ae Merge pull request #7 from adamarthurryan/simple-interface
Simple interface
2015-04-13 23:06:00 -04:00
Adam Brown
3aa913e8c0 cleanup 2015-04-14 02:53:54 +00:00
Adam Brown
b49041db43 simplify interface
remove authentication, document management from front-end
2015-04-12 02:42:47 -04:00
Adam Brown
92fceed3d6 local mongodb 2015-04-12 00:37:38 -04:00
Adam Brown
a7d7f551a7 final fixes 2015-02-28 18:44:36 +00:00
Adam Brown
68cdbe4cc7 fix readme formatting 2015-02-15 16:20:48 -05:00
Adam Brown
ee54bae9d9 refactor to allow better integration between container and local command line 2015-02-15 16:17:28 -05:00
Adam Brown
4470b9caef improvements to ruby gem installation 2015-02-15 15:15:48 -05:00
Adam Brown
97d5f59c3f add questionable run script \n update readme 2015-02-10 01:10:53 -05:00
Adam Brown
4562152a20 add questionable run script \n update readme 2015-02-10 01:10:08 -05:00
Adam Brown
b414f9f181 fix up the buttons\n stabilize ui\n add wdiff panes 2015-02-09 20:47:35 -05:00
Adam Brown
2a4fdeeb96 further ui improvements 2015-02-09 20:19:47 -05:00
Adam Brown
ee3bd40ef7 much improvement to client side \n fixed wdiff output for markdown server side 2015-02-08 20:42:31 -05:00
Adam Brown
4917885686 add rough client interface \n complete server interface for adding revisions \n remove scaffolding examples 2015-02-07 13:49:04 -05:00
Adam Brown
43ea540556 Merge branch 'master' of https://github.com/adamarthurryan/wdiff-markdown-editor 2015-02-06 10:49:38 -05:00
Adam Brown
e84561898f initial check-in with working wdiff server-side api 2015-02-06 10:46:55 -05:00
Adam Brown
515960a3c5 Initial commit 2015-02-06 10:35:22 -05:00
Adam Brown
b385c233b6 transition towards multi-container setup with a startup script 2015-02-03 13:24:34 -05:00
Adam Brown
cb85cb7875 updates to dockerfile 2015-02-01 16:02:11 -05:00
Adam Brown
726af737bd minor 2015-01-26 21:54:44 -05:00
Adam Brown
a3e267cf78 minor 2015-01-26 21:53:18 -05:00
Adam Brown
b554387817 fix readme and comments 2015-01-26 21:52:07 -05:00
Adam Brown
276d034468 cleanup and generate mongod-start script 2015-01-26 21:06:38 -05:00
Adam Brown
11828c2d2e further docker-ify the container definition and add a boot script 2015-01-26 20:07:01 -05:00
Adam Brown
59861bd039 initial port of vagrant box to docker 2015-01-26 17:49:42 -05:00
Adam Brown
2d191fbc00 Initial commit 2015-01-26 11:52:52 -05:00
31 changed files with 654 additions and 636 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Adam Brown
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -39,10 +39,10 @@ To build and launch a dev server:
npm start npm start
npm run server npm run server
To build and launch the distribution server: To build and launch the production server:
npm run build:dist npm run build:prod
npm run server:dist npm run serve:prod
@ -62,8 +62,13 @@ On a low-memory machine, eg. a DigitalOcean 512MB instance, you will need to ena
To make the application start on boot, run the following: To make the application start on boot, run the following:
pm2 start grunt --name dubdiff -- serve:dist # initialize pm2 to start on boot with the systemd boot manager
pm2 startup systemd pm2 startup systemd
# start the app with pm2
pm2 start npm --name dubdiff -- run serve:prod
# save the current pm2 config so that it can be reloaded on boot
pm2 save pm2 save
[Digital Ocean: How To Set Up a Node.js Application for Production on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04) [Digital Ocean: How To Set Up a Node.js Application for Production on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04)

34
TODO.md
View File

@ -1,33 +1 @@
Support for displaying and responding to `#markdown` path suffix.
- create production mode build and serve settings: `webpack.js` and `src/server/babel.index.js`
## State changes
main page edits input documents
compare page views compare documents
compare button:
- generate id
- post input documents to server
- input documents copied to compare documents
- input documents cleared
- go to compare route
edit button:
- compare documents copied to input documents
- compare documents cleared
- go to main route
client start:
- load input documents from localStore
server start:
- load compare documents from database
* client actually never needs to query server for compare documents... huh!

8
dist/main.css vendored
View File

@ -1,3 +1,11 @@
#masthead .header { #masthead .header {
font-size: 4em; font-size: 4em;
} }
ins {
background-color: #dbffdb;
}
del {
background-color: #ffdddd;
}

View File

@ -1,16 +1,19 @@
{ {
"name": "dubdiff-2", "name": "dubdiff",
"version": "0.0.0", "version": "2.0.1",
"description": "", "description": "",
"main": "src/server/babel.index.js", "main": "src/server/babel.index.js",
"scripts": { "scripts": {
"copy-css": "cpy --parents --cwd=./node_modules/semantic-ui-css semantic.min.css themes/default/assets/fonts/icons.woff2 ../../dist", "copy-css": "cpy --parents --cwd=./node_modules/semantic-ui-css semantic.min.css themes/default/assets/fonts/icons.woff2 ../../dist",
"build": "npm run copy-css && webpack --progress --colors", "build": "npm run copy-css && webpack --progress --colors",
"build:prod": "npm run copy-css && cross-env NODE_ENV=production webpack -p --progress --colors", "build:prod": "npm run copy-css && cross-env NODE_ENV=production webpack -p --progress --colors",
"build:prod:nocopy": "cross-env NODE_ENV=production webpack -p --progress --colors",
"build:watch": "npm run copy-css && webpack --progress --colors --watch", "build:watch": "npm run copy-css && webpack --progress --colors --watch",
"serve": "node src/server/babel.index.js", "serve": "node src/server/babel.index.js",
"serve:prod": "cross-env NODE_ENV=production node src/server/babel.index.js", "serve:prod": "cross-env NODE_ENV=production node src/server/babel.index.js",
"webpack-stats": "webpack --json > stats.json", "webpack-stats": "webpack --json > stats.json",
"lint": "standard --verbose | snazzy",
"lint:fix": "standard --fix --verbose | snazzy",
"test": "mocha --watch --compilers js:babel-register" "test": "mocha --watch --compilers js:babel-register"
}, },
"author": "", "author": "",
@ -49,10 +52,13 @@
"babel-register": "^6.18.0", "babel-register": "^6.18.0",
"chai": "^3.5.0", "chai": "^3.5.0",
"copyfiles": "^0.2.2", "copyfiles": "^0.2.2",
"cpy-cli": "^1.0.1",
"cross-env": "^3.1.3", "cross-env": "^3.1.3",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"piping": "^1.0.0-rc.4", "piping": "^1.0.0-rc.4",
"snazzy": "^6.0.0",
"standard": "^8.6.0",
"webpack": "^2.1.0-beta.27" "webpack": "^2.1.0-beta.27"
} }
} }

View File

@ -1,10 +1,7 @@
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.
@ -12,41 +9,40 @@ import * as Selectors from '../common/selectors'
*/ */
// 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

View File

@ -6,20 +6,19 @@ 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 {Format} from '../common/constants'
import * as Actions from '../common/actions' 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 = 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
@ -32,6 +31,46 @@ const store = Redux.createStore(
) )
) )
render()
// read and init the hash after render:
// because this parameter is passed as a hash, it isn't available on the server,
// so the server initial render will be with plaintext, so we have to start with that.
// lame.
initFormatHash(store)
registerFormatListener(store)
// detect hash parameter for markdown/plaintext format and initialize store
function initFormatHash(store) {
// get the has from the window location
let hash = window.location.hash.toUpperCase()
// strip the hash sign
hash = hash.substring(1)
// dispatch the appropriate action
if (hash === Format.MARKDOWN)
store.dispatch(Actions.setMarkdownFormat())
else if (hash === Format.PLAINTEXT || hash === '')
store.dispatch(Actions.setPlaintextFormat())
}
// listen to changes in the redux store and update the url hash parameter when appropriate
function registerFormatListener(store) {
function handleChange() {
let nextFormat = store.getState().format;
if (nextFormat === Format.MARKDOWN)
window.history.replaceState("", document.title, window.location.pathname+"#"+nextFormat.toLowerCase());
else if (nextFormat === Format.PLAINTEXT) {
window.history.replaceState("", document.title, window.location.pathname);
}
}
let unsubscribe = store.subscribe(handleChange);
return unsubscribe;
}
function render () { function render () {
@ -46,5 +85,4 @@ function render() {
, document.getElementById('root')) , document.getElementById('root'))
} }
render()

View File

@ -9,20 +9,22 @@ import {Status, StatusError} from './constants'
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) => {
@ -36,7 +38,6 @@ 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
@ -54,7 +55,6 @@ export const compare = () =>
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) => {
@ -62,20 +62,17 @@ export const reset = () =>
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()
@ -90,17 +87,16 @@ 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})
@ -115,7 +111,7 @@ export const save = () =>
}) })
// 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())

View File

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {Segment, Grid, Form} from 'semantic-ui-react' import {Segment, Grid} from 'semantic-ui-react'
import * as Actions from '../actions'
import * as Selectors from '../selectors' import * as Selectors from '../selectors'
import {Format} from '../constants'
import Header from './Header' import Header from './Header'
import Footer from './Footer' import Footer from './Footer'
import CompareControls from './CompareControls' import CompareControls from './CompareControls'
@ -26,8 +27,6 @@ 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() {
@ -42,25 +41,25 @@ class Compare extends React.Component {
<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>
@ -75,7 +74,6 @@ 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">

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,10 +10,9 @@ 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()),
@ -27,12 +25,12 @@ 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 (
@ -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,8 +3,8 @@ 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>
) )

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,21 +10,19 @@ 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'> <Segment basic padded textAlign='center' as='header' id='masthead'>
<Header><Link onClick={props.onReset}>dubdiff</Link></Header> <Header><Link onClick={props.onReset}>dubdiff</Link></Header>
</Segment> </Segment>
<Rail internal position="right"> <Rail internal position='right'>
<Segment basic padded> <Segment basic padded>
<SaveStatus /> <SaveStatus />
</Segment> </Segment>

View File

@ -13,22 +13,16 @@ 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>
@ -36,34 +30,31 @@ class Main extends React.Component {
<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>
) )
} }
} }

View File

@ -12,7 +12,6 @@ const mapStateToProps = (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()),
@ -24,12 +23,12 @@ const mapDispatchToProps = dispatch => ({
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 (
@ -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>

View File

@ -2,7 +2,6 @@ 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,14 +10,14 @@ 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) {
return (
<Message size='tiny' floating compact icon> <Message size='tiny' floating compact icon>
<Icon name='circle notched' loading /> <Icon name='circle notched' loading />
<Message.Content> <Message.Content>
@ -26,7 +25,9 @@ const SaveStatus = (props) => {
</Message.Content> </Message.Content>
</Message> </Message>
) )
if (props.status.type == Status.LOADING) return ( }
if (props.status.type === Status.LOADING) {
return (
<Message size='tiny' floating compact icon> <Message size='tiny' floating compact icon>
<Icon name='circle notched' loading /> <Icon name='circle notched' loading />
<Message.Content> <Message.Content>
@ -34,7 +35,8 @@ const SaveStatus = (props) => {
</Message.Content> </Message.Content>
</Message> </Message>
) )
else if (props.status.hasError && props.status.errorType == StatusError.SAVE_ERROR) return ( } else if (props.status.hasError && props.status.errorType === StatusError.SAVE_ERROR) {
return (
<Message size='tiny' floating compact icon> <Message size='tiny' floating compact icon>
<Icon name='exclamation' /> <Icon name='exclamation' />
<Message.Content> <Message.Content>
@ -45,7 +47,8 @@ const SaveStatus = (props) => {
</Message.Content> </Message.Content>
</Message> </Message>
) )
else if (props.status.hasError && props.status.errorType == StatusError.LOAD_ERROR) return ( } else if (props.status.hasError && props.status.errorType === StatusError.LOAD_ERROR) {
return (
<Message size='tiny' floating compact icon> <Message size='tiny' floating compact icon>
<Icon name='exclamation' /> <Icon name='exclamation' />
<Message.Content> <Message.Content>
@ -56,9 +59,7 @@ const SaveStatus = (props) => {
</Message.Content> </Message.Content>
</Message> </Message>
) )
} else return (<div />)
else return ( <div></div> )
} }
export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus) export default connect(mapStateToProps, mapDispatchToProps)(SaveStatus)

View File

@ -2,17 +2,16 @@ 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>
@ -18,8 +17,10 @@ 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

@ -1,6 +1,5 @@
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':
@ -25,7 +24,6 @@ export function format (state, action) {
} }
} }
export function show (state, action) { export function show (state, action) {
switch (action.type) { switch (action.type) {
case 'SHOW_ORIGINAL': case 'SHOW_ORIGINAL':
@ -61,15 +59,17 @@ 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 // the error is set in addition to the status
else if (action.type == 'STATUS_SET_ERROR' && isValidError(action.data)) else if (action.type === 'STATUS_SET_ERROR' && isValidError(action.data)) {
return Object.assign({}, state, {error: action.error, hasError: true, errorType: action.data}) return Object.assign({}, state, {error: action.error, hasError: true, errorType: action.data})
else } else {
return state || {type: Status.EMPTY, hasError: false, error: null} 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

@ -4,12 +4,10 @@ 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
@ -25,14 +23,14 @@ 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
} }
) )
@ -40,24 +38,17 @@ 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) {
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final) return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
/* } else if (format === Format.MARKDOWN) {
let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' ')) return Dubdiff.markdownDiff(safeInput.original, safeInput.final)
return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => ( }
part.added ? <ins>{part.value}</ins> :
part.removed ? <del>{part.value}</del> :
<span>{part.value}</span>
))
*/
} }
) )
/* /*
html diff html diff
--- ---

View File

@ -15,12 +15,10 @@ class EditorsDiff extends Diff {
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) {
@ -28,13 +26,13 @@ class EditorsDiff extends Diff {
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: []})
} }
}) })
@ -55,9 +53,7 @@ class EditorsDiff extends Diff {
} }
} }
export default EditorsDiff export default EditorsDiff
const isSpace = str => /[ ]+/.test(str) const isSpace = str => /[ ]+/.test(str)
const isNewline = str => /[\n]+/.test(str)

View File

@ -1,8 +1,7 @@
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) {
@ -21,14 +20,13 @@ export function markdownDiff(original, final) {
// 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 let string = value
if (Array.isArray(value)) if (Array.isArray(value)) {
string = value.join('') string = value.join('')
}
return start + string + end return start + string + end
}).join('') }).join('')
@ -38,16 +36,17 @@ export function diffToHtml(diff) {
return diffToString(diff, {added: {start: '<ins>', end: '</ins>'}, removed: {start: '<del>', end: '</del>'}, same: {start: '', end: ''}}) return diffToString(diff, {added: {start: '<ins>', end: '</ins>'}, removed: {start: '<del>', end: '</del>'}, same: {start: '', end: ''}})
} }
// Rewrites the given diff to correctly render as markdown, assuming the source // Rewrites the given diff to correctly render as markdown, assuming the source
// documents were also valid markdown. // documents were also valid markdown.
// In essence, moves the markdown formatting elements in or out of the inserted and deleted blocks, as appropriate // In essence, moves the markdown formatting elements in or out of the inserted and deleted blocks, as appropriate
// rules: // rules:
// 1. if a multiline del block is followed by an ins block, // 1. if a multiline del block is followed by an ins block,
// the first line of the ins block should be inserted at the end of the first line of the del block // the first line of the ins block should be inserted at the end of the first line of the del block
// so the markdown will apply to the ins text as it should // so the markdown will apply to the ins text as it should
// 2. after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list) // 2. multiline ins and del blocks should be broken up into a series of single line blocks
// 3. after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
// then that prefix should be moved out of the block // then that prefix should be moved out of the block
// not yet implemented rules: // not yet implemented rules:
@ -59,8 +58,10 @@ export function diffToHtml(diff) {
function rewriteMarkdownDiff (diff) { function rewriteMarkdownDiff (diff) {
// apply transformation rules // apply transformation rules
let transformedDiff = diff let transformedDiff = diff
transformedDiff= applyTransformationRule1(transformedDiff) transformedDiff = applyTransformationRuleMultilineDelThenIns(transformedDiff)
transformedDiff= applyTransformationRule2(transformedDiff) transformedDiff = applyTransformationRuleBreakUpDelIns(transformedDiff)
transformedDiff = applyTransformationRuleFormattingPrefix(transformedDiff)
transformedDiff = applyTransformationRuleRemoveEmpty(transformedDiff)
return transformedDiff return transformedDiff
} }
@ -68,10 +69,13 @@ function rewriteMarkdownDiff(diff) {
// 1. if a multiline del block is followed by an ins block, // 1. if a multiline del block is followed by an ins block,
// the first line of the ins block should be inserted at the end of the first line of the del block // the first line of the ins block should be inserted at the end of the first line of the del block
// so the markdown will apply to the ins text as it should // so the markdown will apply to the ins text as it should
function applyTransformationRule1(diff) { function applyTransformationRuleMultilineDelThenIns (diff) {
let transformedDiff = [] let transformedDiff = []
const B_ADDED='added', B_REMOVED='removed', B_SAME='same' const B_ADDED = 'added'
const B_REMOVED = 'removed'
const B_SAME = 'same'
let previousBlockType = null let previousBlockType = null
let currentBlockType = null let currentBlockType = null
let previousBlockWasMultiline = false let previousBlockWasMultiline = false
@ -79,7 +83,6 @@ function applyTransformationRule1(diff) {
// 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))
@ -88,8 +91,7 @@ function applyTransformationRule1(diff) {
// 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)
@ -99,16 +101,14 @@ function applyTransformationRule1(diff) {
// split the first line from the previous block // split the first line from the previous block
let previousBlockSplit = splitMultilineDiffBlock(previousBlock) let previousBlockSplit = splitMultilineDiffBlock(previousBlock)
// now add the blocks back, interleaving del and ins blocks // now add the blocks back, interleaving del and ins blocks
for (let i = 0; i < Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) { for (let i = 0; i < Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
if (i<previousBlockSplit.length) if (i < previousBlockSplit.length) {
transformedDiff.push(previousBlockSplit[i]) transformedDiff.push(previousBlockSplit[i])
if (i<currentBlockSplit.length)
transformedDiff.push(currentBlockSplit[i])
} }
if (i < currentBlockSplit.length) { transformedDiff.push(currentBlockSplit[i]) }
} }
else { } else {
// otherwise, we just add the current block to the transformed list // otherwise, we just add the current block to the transformed list
transformedDiff.push(currentBlock) transformedDiff.push(currentBlock)
} }
@ -117,6 +117,43 @@ function applyTransformationRule1(diff) {
return transformedDiff return transformedDiff
} }
// Transformation rule 2
// 2. multiline del and ins blocks should be broken up
// into a series of single line blocks
function applyTransformationRuleBreakUpDelIns (diff) {
let transformedDiff = []
const B_ADDED = 'added'
const B_REMOVED = 'removed'
const B_SAME = 'same'
let blockType = null
let blockIsMultiline = false
// iterate the input tokens to create the intermediate representation
diff.forEach((block) => {
blockType = (block.added ? B_ADDED : (block.removed ? B_REMOVED : B_SAME))
blockIsMultiline = isMultilineDiffBlock(block)
// transform rule applys when:
// the current block is an ins or del and is multiline
if ((blockType === B_REMOVED || blockType === B_ADDED) && blockIsMultiline) {
// split the first line from the current block
let blockSplit = splitMultilineDiffBlock(block)
blockSplit.forEach(blockSplitLine => transformedDiff.push(blockSplitLine))
} else {
// otherwise, we just add the current block to the transformed list
transformedDiff.push(block)
}
})
return transformedDiff
}
// Transformation rule number 4: remove empty blocks
function applyTransformationRuleRemoveEmpty (diff) {
return diff.filter(({value}) => value.length > 0)
}
// matches markdown prefixes that affect the formatting of the whole subsequent line // matches markdown prefixes that affect the formatting of the whole subsequent line
// ^ - start of line // ^ - start of line
@ -126,18 +163,18 @@ function applyTransformationRule1(diff) {
// |([ \t]+[\*\+-]) - unordered lists // |([ \t]+[\*\+-]) - unordered lists
// |([ \t]+[0-9]+\.) - numeric lists // |([ \t]+[0-9]+\.) - numeric lists
// )? // )?
// [ \t]* - trailing whitespace // [ \t]+ - trailing whitespace
const MARKDOWN_PREFIX = /^([ \t]*\>)*(([ \t]*#*)|([ \t]*[\*\+-])|([ \t]*[\d]+\.))?[ \t]*/ const MARKDOWN_PREFIX = /^([ \t]*>)*(([ \t]*#*)|([ \t]*[*+\-])|([ \t]*[\d]+\.))?[ \t]+/
// matches strings that end with a newline followed by some whitespace // matches strings that end with a newline followed by some whitespace
const NEWLINE_SUFFIX = /\n\s*$/ const NEWLINE_SUFFIX = /\n\s*$/
// transformation rule 2: // transformation rule 3:
// after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list) // after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
// then that prefix should be moved out of the block // then that prefix should be moved out of the block
// also, if an ins block begins with a formatting prefix and follows immediately after a del block that follows a newline, // also, if an ins block begins with a formatting prefix and follows immediately after a del block that follows a newline,
// the prefix should be moved out of the block _and_ an extra newline character should be added to the beginning of it // the prefix should be moved out of the block _and_ an extra newline character should be added to the beginning of it
function applyTransformationRule2(diff) { function applyTransformationRuleFormattingPrefix (diff) {
let transformedDiff = [] let transformedDiff = []
let isNewline = true let isNewline = true
@ -145,7 +182,6 @@ function applyTransformationRule2(diff) {
// 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) {
@ -158,70 +194,42 @@ function applyTransformationRule2(diff) {
} }
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 // returns true if the given diff block contains a newline element
function isMultilineDiffBlock ({value}) { function isMultilineDiffBlock ({value}) {
return value.indexOf('\n') != -1 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,
// each of which subsequent to the first block will begin with a newline // 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 // 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}) {
//find the indices of the diff block that coorespond to newlines let lines = value.split('\n')
const splits = indicesOf(value, c => (c=='\n') ) let blocks = []
// lines = lines.filter(line=>line.length>0)
splits.push(value.length) lines.forEach((line, index) => {
blocks.push({added, removed, value: line})
//create a range from each index if (index < lines.length - 1) blocks.push({value: '\n'})
const ranges = splits.reduce( })
//the accumulator is a structure with the last index and the list of ranges
//the ranges are a {start, end} structure
({last, ranges}, i) => {
ranges = ranges.concat([{start:last, end:i}])
return {last:i, ranges}
},
//start with the zero index and an empty array
{last: 0, ranges:[]}
).ranges
//map the ranges into blocks
const blocks = ranges.map(
//each block is the same as the given original block, but with the values split at newlines
({start, end}) => ({added, removed, value:value.substring(start, end)})
)
//console.log({value, splits, ranges, blocks})
return blocks return blocks
} }
//collect all the indices of the given string that satisfy the test function
const indicesOf = (string, test) => string.split('').reduce(
//add indexes that satisfy the test function to the array
(acc, x, i) => (test(x) ? acc.concat([i]) : acc ),
//start with the empty array
[]
)

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)
var piping = require('piping')
main()
function main () {
// Enable piping for non-production environments // Enable piping for non-production environments
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== 'production') {
if (!require("piping")({hook: true, includeModules: false})) { // piping will return false for the initial invocation
return; // the app will be run again in an instance managed by piping
if (!piping({hook: true, includeModules: false})) {
return
} }
} }
try { try {
require('./index.js'); require('./index.js')
} catch (error) {
console.error(error.stack)
} }
catch (error) {
console.error(error.stack);
} }

View File

@ -3,7 +3,6 @@ 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)
@ -56,7 +55,6 @@ function readRecord(res, id, 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)
@ -67,13 +65,12 @@ function writeRecord(res, id, data) {
// 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)
)) ))
}) })
} }
@ -85,7 +82,6 @@ function handleError(res, 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,14 +7,14 @@ import fetch from 'isomorphic-fetch'
import comparisonRouter from './comparison' import comparisonRouter from './comparison'
import * as reducers from '../common/reducers' import * as reducers from '../common/reducers'
import {Status, StatusError} from '../common/constants' import {Status, StatusError} from '../common/constants'
import render from './render' import render from './render'
const PORT = 8080 // set use port 8080 for dev, 80 for production
const PORT = (process.env.NODE_ENV !== 'production' ? 8080 : 80)
const app = express() const app = express()
@ -23,26 +23,22 @@ 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 // the following routes are for server-side rendering of the app
// we should render the comparison directly from the server // we should render the comparison directly from the server
// this loading logic could be moved into ../common/actions because it is isomorphic // 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)
@ -55,19 +51,16 @@ app.route('/:comparisonId')
.catch(error => { .catch(error => {
initAndRenderError(error, store, req, res) initAndRenderError(error, store, req, res)
}) })
}) })
app.route('/') app.route('/')
.get((req, res) => { .get((req, res) => {
render(createSessionStore(), req, res) render(createSessionStore(), req, res)
}) })
app.listen(PORT, function () { app.listen(PORT, function () {
console.log('Server listening on port 8080.') console.log(`Server listening on port ${PORT}.`)
}) })
// creates the session store // creates the session store
function createSessionStore () { function createSessionStore () {
// create the redux store // create the redux store

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) {
@ -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.`))
} }
@ -54,7 +51,6 @@ const pageTemplate = (body) => {
<link rel="icon" type="image/png" sizes="96x96" href="dist/favicon-96x96.png"> <link rel="icon" type="image/png" sizes="96x96" href="dist/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="dist/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="dist/favicon-16x16.png">
</head> </head>
<body> <body>
${body} ${body}
@ -67,21 +63,19 @@ 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) { function appTemplate (html, initialState) {
return pageTemplate(` return pageTemplate(`
<div id="root">${html}</div> <div id="root">${html}</div>
<script> <script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)} window.__INITIAL_STATE__ = "${encodeURI(JSON.stringify(initialState, null, 2))}"
</script> </script>
<!-- <script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script> --> <!-- <script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script> -->
<script type="text/javascript" src="dist/browser-bundle.js"></script> <script type="text/javascript" src="dist/browser-bundle.js"></script>

View File

@ -1,7 +1,7 @@
/* 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'
@ -9,13 +9,11 @@ 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(
@ -43,7 +41,7 @@ describe('dubdiff', () => {
`# Title `# Title
other`, other`,
`# Subtitle` `# Subtitle`
)).to.equal('# [-Title-]{+Subtitle+}[-\nother-]') )).to.equal('# [-Title-]{+Subtitle+}\n[-other-]')
}) })
it('pulls prefixes out of ins or del blocks after newline', () => { it('pulls prefixes out of ins or del blocks after newline', () => {
@ -71,4 +69,17 @@ other`,
'This [link](https://somewhere.org) changed.' 'This [link](https://somewhere.org) changed.'
)).to.equal('This [link](https://somewhere.[-com-]{+org+}) [-is the same-]{+changed+}.') )).to.equal('This [link](https://somewhere.[-com-]{+org+}) [-is the same-]{+changed+}.')
}) })
it('deletes a title', () => {
expect(diff(
'Hello\n# Title 1\n# Title 2',
'Hello\n# Title 2'
)).to.equal('Hello\n# Title [-1-]\n# [-Title -]2')
}) })
it('deletes a more different title', () => {
expect(diff(
'Hello\n# Filbert\n# Title 2',
'Hello\n# Title 2'
)).to.equal('Hello\n# [-Filbert-]\n# Title 2')
})
})

View File

@ -1,7 +1,7 @@
/* 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'
@ -9,12 +9,11 @@ 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(

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