add semantic css local, begin implement markdown diff, etc.
This commit is contained in:
parent
27582ef871
commit
55a30797ec
33
TODO.md
33
TODO.md
@ -1,2 +1,33 @@
|
|||||||
|
|
||||||
- create production mode build and serve settings: `webpack.js` and `src/server/babel.index.js`
|
- 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!
|
16278
dist/browser-bundle.js
vendored
16278
dist/browser-bundle.js
vendored
File diff suppressed because one or more lines are too long
11
dist/semantic.min.css
vendored
Normal file
11
dist/semantic.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -4,8 +4,9 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/server/babel.index.js",
|
"main": "src/server/babel.index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ebpack --colors",
|
"copy-css": "copyfiles -f ./node_modules/semantic-ui-css/semantic.min.css ./dist",
|
||||||
"start": "webpack --progress --colors --watch",
|
"build": "npm run copy-css && webpack --colors",
|
||||||
|
"start": "npm run copy-css && webpack --progress --colors --watch",
|
||||||
"serve": "node src/server/babel.index.js",
|
"serve": "node src/server/babel.index.js",
|
||||||
"webpack-stats": "webpack --profile --json > stats.json",
|
"webpack-stats": "webpack --profile --json > stats.json",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"redux": "^3.5.1",
|
"redux": "^3.5.1",
|
||||||
"redux-router": "^1.0.0-beta5",
|
"redux-router": "^1.0.0-beta5",
|
||||||
"reselect": "^2.5.1",
|
"reselect": "^2.5.1",
|
||||||
|
"semantic-ui-css": "^2.2.4",
|
||||||
"semantic-ui-react": "^0.61.6",
|
"semantic-ui-react": "^0.61.6",
|
||||||
"uuid": "^3.0.1"
|
"uuid": "^3.0.1"
|
||||||
},
|
},
|
||||||
|
@ -13,14 +13,27 @@ import * as reducers from '../common/reducers'
|
|||||||
import routes from '../common/routes'
|
import routes from '../common/routes'
|
||||||
|
|
||||||
|
|
||||||
|
//the localStore implementation is naive
|
||||||
|
//initial state should be rehydrated from the server
|
||||||
|
//then additional state transformations should be applied based on localStore contents
|
||||||
|
// (or not? maybe localStore is not needed)
|
||||||
|
|
||||||
|
const initialState = 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),
|
||||||
localStore.get("dubdiff"),
|
initialState,
|
||||||
window.devToolsExtension ? window.devToolsExtension() : undefined
|
window.devToolsExtension ? window.devToolsExtension() : undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const localInput = localStore.get('dubdiff')
|
||||||
|
if (localInput.input) {
|
||||||
|
//dispatch localStore data to store
|
||||||
|
//should this be done after the first render?
|
||||||
|
}
|
||||||
|
|
||||||
//save the state whenever the state changes
|
//save the state whenever the state changes
|
||||||
function saveState() {
|
function saveState() {
|
||||||
let state = store.getState()
|
let state = store.getState()
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
|
||||||
export const resetInput = () => ({ type: 'RESET_INPUT' })
|
|
||||||
export const updateOriginalInput = (text) => ({ type: 'UPDATE_ORIGINAL_INPUT', data:text})
|
export const updateOriginalInput = (text) => ({ type: 'UPDATE_ORIGINAL_INPUT', data:text})
|
||||||
export const updateFinalInput = (text) => ({ type: 'UPDATE_FINAL_INPUT', data:text})
|
export const updateFinalInput = (text) => ({ type: 'UPDATE_FINAL_INPUT', data:text})
|
||||||
|
export const clearInput = () => ({ type: 'CLEAR_INPUT'})
|
||||||
|
export const updateOriginalCompare = (text) => ({ type: 'UPDATE_ORIGINAL_COMPARE', data:text})
|
||||||
|
export const updateFinalCompare = (text) => ({ type: 'UPDATE_FINAL_COMPARE', data:text})
|
||||||
|
export const clearCompare = () => ({ type: 'CLEAR_COMPARE'})
|
||||||
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'})
|
||||||
|
@ -17,7 +17,7 @@ const mapStateToProps = (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),
|
compare: state.compare,
|
||||||
diff: Selectors.diff(state)
|
diff: Selectors.diff(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ class Compare extends React.Component {
|
|||||||
<ShowPlaintext diff={this.props.diff} isMarkdownFormat={this.props.isMarkdownFormat}>{this.props.diff}</ShowPlaintext>:
|
<ShowPlaintext diff={this.props.diff} isMarkdownFormat={this.props.isMarkdownFormat}>{this.props.diff}</ShowPlaintext>:
|
||||||
|
|
||||||
<ShowPlaintext
|
<ShowPlaintext
|
||||||
text={this.props.isShowOriginal? this.props.safeInput.original: this.props.safeInput.final}
|
text={this.props.isShowOriginal? this.props.compare.original: this.props.compare.final}
|
||||||
isMarkdownFormat={this.props.isMarkdownFormat}
|
isMarkdownFormat={this.props.isMarkdownFormat}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ const mapStateToProps = (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),
|
||||||
|
compare: state.compare
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -21,13 +22,17 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
onShowOriginal: () => dispatch(Actions.showOriginal()),
|
onShowOriginal: () => dispatch(Actions.showOriginal()),
|
||||||
onShowFinal: () => dispatch(Actions.showFinal()),
|
onShowFinal: () => dispatch(Actions.showFinal()),
|
||||||
onShowDifference: () => dispatch(Actions.showDifference()),
|
onShowDifference: () => dispatch(Actions.showDifference()),
|
||||||
|
onEdit: (compare) => {
|
||||||
|
dispatch(Actions.updateOriginalInput(compare.original))
|
||||||
|
dispatch(Actions.updateFinalInput(compare.final))
|
||||||
|
dispatch(Actions.clearCompare())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
class CompareControls extends React.Component {
|
class CompareControls extends React.Component {
|
||||||
|
|
||||||
onClickCompare() {
|
onClickEdit() {
|
||||||
|
this.props.onEdit(this.props.compare)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickMarkdownFormat() {
|
onClickMarkdownFormat() {
|
||||||
@ -41,6 +46,10 @@ class CompareControls extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Segment.Group>
|
<Segment.Group>
|
||||||
|
<Segment>
|
||||||
|
<Link to="/"><Button fluid onClick={this.onClickEdit.bind(this)}>Edit</Button></Link>
|
||||||
|
</Segment>
|
||||||
|
|
||||||
<Segment >
|
<Segment >
|
||||||
<Button fluid onClick={this.props.onShowOriginal} active={this.props.isShowOriginal}>Original</Button>
|
<Button fluid onClick={this.props.onShowOriginal} active={this.props.isShowOriginal}>Original</Button>
|
||||||
<Button fluid onClick={this.props.onShowFinal} active={this.props.isShowFinal}>Final</Button>
|
<Button fluid onClick={this.props.onShowFinal} active={this.props.isShowFinal}>Final</Button>
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {connect} from 'react-redux'
|
||||||
|
|
||||||
import {Segment, Header} from 'semantic-ui-react'
|
import {Segment, Header} from 'semantic-ui-react'
|
||||||
|
|
||||||
import {Link} from 'react-router'
|
import {Link} from 'react-router'
|
||||||
|
|
||||||
|
import * as Actions from '../actions'
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onClear: () => {
|
||||||
|
dispatch(Actions.clearInput())
|
||||||
|
dispatch(Actions.clearCompare())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const SiteHeader = (props) => (
|
const SiteHeader = (props) => (
|
||||||
<Segment basic padded textAlign="center" as="header" id='masthead'>
|
<Segment basic padded textAlign="center" as="header" id='masthead'>
|
||||||
<Header><Link to="/">dubdiff</Link></Header>
|
<Link to="/"><Header onClick={props.onClear}>dubdiff</Header></Link>
|
||||||
</Segment>
|
</Segment>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default SiteHeader
|
export default connect(mapStateToProps, mapDispatchToProps)(SiteHeader)
|
||||||
|
@ -9,19 +9,29 @@ 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),
|
||||||
|
safeInput: Selectors.safeInput(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
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()),
|
||||||
|
onCompare: (safeInput) => {
|
||||||
|
dispatch(Actions.updateOriginalCompare(safeInput.original))
|
||||||
|
dispatch(Actions.updateFinalCompare(safeInput.final))
|
||||||
|
dispatch(Actions.clearInput())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
class MainControls extends React.Component {
|
class MainControls extends React.Component {
|
||||||
|
|
||||||
onClickCompare() {
|
onClickCompare() {
|
||||||
|
//generate new id? (or should the id be baked into the link route?)
|
||||||
|
//post safeInput to db
|
||||||
|
|
||||||
|
this.props.onCompare(this.props.safeInput)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickMarkdownFormat() {
|
onClickMarkdownFormat() {
|
||||||
@ -36,7 +46,7 @@ class MainControls extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Segment.Group>
|
<Segment.Group>
|
||||||
<Segment >
|
<Segment >
|
||||||
<Link to="compare"><Button fluid>Compare</Button></Link>
|
<Link to="compare"><Button fluid onClick={this.onClickCompare.bind(this)}>Compare</Button></Link>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|
||||||
<Segment >
|
<Segment >
|
||||||
|
@ -17,10 +17,10 @@ const ShowPlaintext = (props) => {
|
|||||||
export default ShowPlaintext
|
export default ShowPlaintext
|
||||||
|
|
||||||
function diffToPre(diff) {
|
function diffToPre(diff) {
|
||||||
return diff.map(part => (
|
return diff.map((part, index) => (
|
||||||
part.added ? <span><ins>{part.value}</ins>{ifNotNewlineSpace(part.value)}</span> :
|
part.added ? <span key={index}><ins>{part.value}</ins>{ifNotNewlineSpace(part.value)}</span> :
|
||||||
part.removed ? <span><del>{part.value}</del>{ifNotNewlineSpace(part.value)}</span> :
|
part.removed ? <span key={index}><del>{part.value}</del>{ifNotNewlineSpace(part.value)}</span> :
|
||||||
<span>{part.value}{ifNotNewlineSpace(part.value)}</span>
|
<span key={index}>{part.value}{ifNotNewlineSpace(part.value)}</span>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,33 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 'RESET_INPUT':
|
case 'CLEAR_INPUT':
|
||||||
return {original:'', final:''}
|
return {original:'', final:''}
|
||||||
default:
|
default:
|
||||||
return state || {original:'', final:''}
|
return state || {original:'', final:''}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function compare (state, action ) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'UPDATE_ORIGINAL_COMPARE':
|
||||||
|
return Object.assign({}, state, {original:action.data})
|
||||||
|
case 'UPDATE_FINAL_COMPARE':
|
||||||
|
return Object.assign({}, state, {final:action.data})
|
||||||
|
case 'CLEAR_COMPARE':
|
||||||
|
return {original:'', final:''}
|
||||||
|
default:
|
||||||
|
return state || {original:'', final:''}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const Format = {
|
export const Format = {
|
||||||
PLAINTEXT: 'PLAINTEXT',
|
PLAINTEXT: 'PLAINTEXT',
|
||||||
MARKDOWN: 'MARKDOWN'
|
MARKDOWN: 'MARKDOWN'
|
||||||
|
@ -5,8 +5,8 @@ import Main from './components/Main'
|
|||||||
import Compare from './components/Compare'
|
import Compare from './components/Compare'
|
||||||
|
|
||||||
var routes = [
|
var routes = [
|
||||||
<Route path="/" component={Main}/>,
|
<Route key="root" path="/" component={Main}/>,
|
||||||
<Route path="/:compareId" component={Compare}/>
|
<Route key="compare" path="/:compareId" component={Compare}/>
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ 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
|
||||||
|
const compare = (state) => state.compare
|
||||||
|
|
||||||
export const safeInput = createSelector(
|
export const safeInput = createSelector(
|
||||||
[input],
|
[input],
|
||||||
@ -42,9 +43,9 @@ export const isShowDifference= isShow(Show.DIFFERENCE)
|
|||||||
|
|
||||||
|
|
||||||
export const diff = createSelector(
|
export const diff = createSelector(
|
||||||
[format, input],
|
[format, compare],
|
||||||
(format, input) => {
|
(format, compare) => {
|
||||||
return Dubdiff.plaintextDiff(input.original, input.final)
|
return Dubdiff.plaintextDiff(compare.original, compare.final)
|
||||||
/*
|
/*
|
||||||
let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' '))
|
let diff = JsDiff.diffWords (input.original.replace(/ /g, ' '), input.final.replace(/ /g, ' '))
|
||||||
return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => (
|
return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/ /g, ' ')})).map(part => (
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import * as JsDiff from 'diff'
|
import * as JsDiff from 'diff'
|
||||||
|
|
||||||
|
|
||||||
|
//!!! this deal with adding and removing spaces could be done more elegantly by
|
||||||
|
// diffing on an array of simple data structures that contain the text and the adjacent space
|
||||||
|
// the diff would use a custom compare function that would disregard the spaces
|
||||||
|
// alternately, the text could be split with the spaces included in the array and then compared with a
|
||||||
|
// custom diff function that would treat the space elements as null/ignored
|
||||||
export function plaintextDiff(original, final) {
|
export function plaintextDiff(original, final) {
|
||||||
|
|
||||||
|
|
||||||
let arrOriginal = plaintextSplit(original)
|
let arrOriginal = plaintextSplit(original)
|
||||||
let arrFinal = plaintextSplit(final)
|
let arrFinal = plaintextSplit(final)
|
||||||
|
|
||||||
@ -11,13 +14,17 @@ export function plaintextDiff(original, final) {
|
|||||||
diff = plaintextRestoreSpaces(diff)
|
diff = plaintextRestoreSpaces(diff)
|
||||||
|
|
||||||
return diff
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
// return JsDiff.diffWordsWithSpace(original,final)
|
export function markdownDiff(original, final) {
|
||||||
|
let arrOriginal = plaintextSplit(original)
|
||||||
|
let arrFinal = plaintextSplit(final)
|
||||||
|
|
||||||
|
let diff = JsDiff.diffArrays(arrOriginal, arrFinal)
|
||||||
|
diff = plaintextRestoreSpaces(diff)
|
||||||
|
diff = rewriteMarkdownDiff(diff)
|
||||||
|
|
||||||
// let diff = JsDiff.diffLines(original.replace(/ /g, '###\n'), final.replace(/ /g, '###\n'))
|
return diff
|
||||||
// console.log(diff, diff.map(({added, removed, value})=>({added, removed, value:value.replace(/###\n/g, ' ')})))
|
|
||||||
// return diff.map(({added, removed, value})=>({added, removed, value:value.replace(/###\n/g, ' ')}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -37,4 +44,199 @@ function plaintextRestoreSpaces (diff) {
|
|||||||
(str!='\n' && (idx<arr.length-1)) ? str+" " : str)
|
(str!='\n' && (idx<arr.length-1)) ? str+" " : str)
|
||||||
).join('')
|
).join('')
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Rewrites the given diff to correctly render as markdown, assuming the source
|
||||||
|
// documents were also valid markdown.
|
||||||
|
// In essence, moves the markdown formatting elements in or out of the inserted and deleted blocks, as appropriate
|
||||||
|
|
||||||
|
//rules:
|
||||||
|
// 1. if a multiline del block is followed by an ins block,
|
||||||
|
// the first line of the ins block should be inserted at the end of the first line of the del block
|
||||||
|
// so the markdown will apply to the ins text as it should
|
||||||
|
// 2. after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
|
||||||
|
// then that prefix should be moved out of the block
|
||||||
|
|
||||||
|
//not yet implemented rules:
|
||||||
|
// 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
|
||||||
|
// the block should be broken up to move the formatting code outside
|
||||||
|
// OR the whole formatting string could be brought into the block
|
||||||
|
// eg. **Hello <del>World</del><ins>Darling</ins>** <ins>I</ins><del>she</del> said
|
||||||
|
function rewriteMarkdownDiff(diff) {
|
||||||
|
//apply transformation rules
|
||||||
|
let transformedDiff = diff
|
||||||
|
transformedDiff= applyTransformationRule1(transformedDiff)
|
||||||
|
//transformedDiff= applyTransformationRule2(transformedDiff)
|
||||||
|
return transformedDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
//Transformation rule 1
|
||||||
|
// 1. if a multiline del block is followed by an ins block,
|
||||||
|
// the first line of the ins block should be inserted at the end of the first line of the del block
|
||||||
|
// so the markdown will apply to the ins text as it should
|
||||||
|
function applyTransformationRule1(diff) {
|
||||||
|
let transformedDiff = []
|
||||||
|
|
||||||
|
const B_ADD='added', B_REM='removed', B_SAME='same'
|
||||||
|
let previousBlockType = null
|
||||||
|
let currentBlockType = null
|
||||||
|
let previousBlockWasMultiline = false
|
||||||
|
let currentBlockIsMultiline = false
|
||||||
|
|
||||||
|
//iterate the input tokens to create the intermediate representation
|
||||||
|
diff.forEach((currentBlock) => {
|
||||||
|
|
||||||
|
previousBlockType = currentBlockType
|
||||||
|
previousBlockWasMultiline = currentBlockIsMultiline
|
||||||
|
currentBlockType = (currentBlock.added ? B_ADD : (currentBlock.removed ? B_REMOVED : B_SAME))
|
||||||
|
currentBlockIsMultiline = isMultilineDiffBlock(currentBlock)
|
||||||
|
|
||||||
|
//transform rule 1 applys when:
|
||||||
|
// the previous block was a del and had multiple lines
|
||||||
|
// the current block is an ins
|
||||||
|
if (previousBlockType == B_REM && currentBlockType == B_INS && previousBlockWasMultiline) {
|
||||||
|
//split the first line from the current block
|
||||||
|
let currentBlockSplit = splitMultilineDiffBlock(currentBlock)
|
||||||
|
|
||||||
|
//pop the previous diff entry
|
||||||
|
let previousBlock = transformedDiff.pop()
|
||||||
|
|
||||||
|
//split the first line from the previous block
|
||||||
|
let previousBlockSplit = splitMultilineDiffBlock(currentBlock)
|
||||||
|
|
||||||
|
//now add the blocks back, interleaving del and ins blocks
|
||||||
|
for (let i=0; i<Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
|
||||||
|
if (i<previousBlockSplit.length)
|
||||||
|
transformedDiff.push(previousBlockSplit[i])
|
||||||
|
|
||||||
|
if (i<currentBlockSplit.length)
|
||||||
|
transformedDiff.push(currentBlockSplit[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//otherwise, we just add the current block to the transformed list
|
||||||
|
transformedDiff.push(currentBlock)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return transformedDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTransformationRule2(diff) {
|
||||||
|
// we need to find markdown prefixes that should be pulled out of added/removed blocks to the start of the line
|
||||||
|
// prefixes are matched as follows:
|
||||||
|
// ^ - start of line
|
||||||
|
// ([ \t]*\>)* - blockquotes (possibly nested)
|
||||||
|
// (
|
||||||
|
// ([ \t]*#*) - headers
|
||||||
|
// |([ \t]+[\*\+-]) - unordered lists
|
||||||
|
// |([ \t]+[0-9]+\.) - numeric lists
|
||||||
|
// )?
|
||||||
|
// [ \t]* - trailing whitespace
|
||||||
|
const PREFIX = /^([ \t]*\>)*(([ \t]*#*)|([ \t]*[\*\+-])|([ \t]*[\d]+\.))?[ \t]*/
|
||||||
|
|
||||||
|
let transformedDiff = []
|
||||||
|
return transformedDiff
|
||||||
|
|
||||||
|
|
||||||
|
/// ...
|
||||||
|
transform.forEach(function(item) {
|
||||||
|
//newlines are undecorated
|
||||||
|
if (item.string == '\n') {
|
||||||
|
output += '\n';
|
||||||
|
|
||||||
|
//flag the new line
|
||||||
|
newline = true;
|
||||||
|
//and record the offset in the output string
|
||||||
|
newlineIndex = output.length;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//wrap del strings with tags
|
||||||
|
if (item.state == SDEL) {
|
||||||
|
output += '<del>' + item.string + '</del>';
|
||||||
|
//del doesn't reset the newline state
|
||||||
|
}
|
||||||
|
|
||||||
|
//ins strings have to be handled a little differently:
|
||||||
|
//if this is an ins just after a newline, or after a del after a newline,
|
||||||
|
//we need to peel off any markdown formatting prefixes and insert them at the beginning of the line outside the del/ins tags
|
||||||
|
else if (item.state == SINS && newline) {
|
||||||
|
var prestring, poststring;
|
||||||
|
var match = item.string.match(PREFIX);
|
||||||
|
if (match == null)
|
||||||
|
prestring ="";
|
||||||
|
else
|
||||||
|
prestring = match[0];
|
||||||
|
|
||||||
|
poststring = item.string.substring(prestring.length);
|
||||||
|
|
||||||
|
output = output.substring(0, newlineIndex) + prestring + output.substring(newlineIndex);
|
||||||
|
output += '<ins>' + poststring + '</ins>';
|
||||||
|
newline = false;
|
||||||
|
newlineIndex = -1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (item.state == SINS) {
|
||||||
|
output += '<ins>' + item.string + '</ins>';
|
||||||
|
}
|
||||||
|
|
||||||
|
//and just output other strings
|
||||||
|
else {
|
||||||
|
output += item.string;
|
||||||
|
//this resets the newline state
|
||||||
|
newline = false;
|
||||||
|
newlineIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//returns true if the given diff block contains a newline element
|
||||||
|
function isMultilineDiffBlock({value}) {
|
||||||
|
return value.find(word => word == '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//returns an array of diff blocks that have the same added, removed fields as the given one
|
||||||
|
//but with the array of words split by newlines
|
||||||
|
//if the diff block has no newlines, an array containing only that diff will be returned
|
||||||
|
//if the diff block has newlines, the resulting array will have a series of blocks,
|
||||||
|
// each of which subsequent to the first block will begin with a newline
|
||||||
|
//if the diff block begins with a newline, the returned array will begin with an empty diff
|
||||||
|
function splitMultilineDiffBlock({added, removed, value}) {
|
||||||
|
//find the indices of the diff block that coorespond to newlines
|
||||||
|
const splits = findIndicesOf(value, '\n')
|
||||||
|
|
||||||
|
//create a range from each index
|
||||||
|
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) => {i, ranges.concat([{start:last, end:i}])},
|
||||||
|
//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.slice(start, end)}
|
||||||
|
)
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
//collect all the indices of the given array that satisfy the test function
|
||||||
|
const findIndicesOf = (array, test) => array.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
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
@ -34,7 +34,7 @@ export default function render(store, req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send(renderError('Not found', ''))
|
res.status(404).send(errorTemplate('Not found', `${req.url} not found.`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -47,7 +47,7 @@ const pageTemplate = (body) => {
|
|||||||
<title>Dubdiff</title>
|
<title>Dubdiff</title>
|
||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.2/semantic.min.css"/>
|
<link rel="stylesheet" href="dist/semantic.min.css"/>
|
||||||
<link rel="stylesheet" href="dist/main.css"/>
|
<link rel="stylesheet" href="dist/main.css"/>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
@ -65,7 +65,10 @@ function errorTemplate(title, message, exception) {
|
|||||||
return pageTemplate(`
|
return pageTemplate(`
|
||||||
<h1>${title}</h1>
|
<h1>${title}</h1>
|
||||||
<p>${message}</p>
|
<p>${message}</p>
|
||||||
<pre>${exception.toString()}</pre>
|
${exception ?
|
||||||
|
`<pre>${exception.toString()}</pre>`:
|
||||||
|
``
|
||||||
|
}
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user