Compare commits

..

No commits in common. "master" and "version-1" have entirely different histories.

91 changed files with 3962 additions and 1821 deletions

View File

@ -1,3 +0,0 @@
{
"presets": ["node6", "react"],
}

3
.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "client/bower_components"
}

1
.buildignore Normal file
View File

@ -0,0 +1 @@
*.coffee

21
.editorconfig Normal file
View File

@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

22
.gitignore vendored
View File

@ -1,13 +1,11 @@
*~
bin
node_modules
data/*
dist/themes
dist/semantic.min.css
browser-bundle.js
browser-bundle.js.map
npm-debug.log.*
stats.json
stats.analyzed.txt
public
.tmp
.sass-cache
.idea
client/bower_components
dist
server/config/local.env.js
wdiff-1.2.2
data

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: node_js
node_js:
- '0.10'
- '0.11'
before_script:
- npm install -g bower grunt-cli
- gem install sass
- bower install
services: mongodb

51
.yo-rc.json Normal file
View File

@ -0,0 +1,51 @@
{
"generator-angular-fullstack": {
"insertRoutes": true,
"registerRoutesFile": "server/routes.js",
"routesNeedle": "// Insert routes below",
"routesBase": "/api/",
"pluralizeRoutes": true,
"insertSockets": true,
"registerSocketsFile": "server/config/socketio.js",
"socketsNeedle": "// Insert sockets below",
"filters": {
"js": true,
"jade": true,
"sass": true,
"ngroute": true,
"bootstrap": true,
"uibootstrap": true,
"mongoose": true,
"auth": true,
"oauth": true,
"googleAuth": true,
"facebookAuth": true,
"twitterAuth": true
}
},
"generator-ng-component": {
"routeDirectory": "client/app/",
"directiveDirectory": "client/app/",
"filterDirectory": "client/app/",
"serviceDirectory": "client/app/",
"basePath": "client",
"moduleName": "",
"filters": [
"ngroute"
],
"extensions": [
"js",
"jade",
"scss"
],
"directiveSimpleTemplates": "",
"directiveComplexTemplates": "",
"filterTemplates": "",
"serviceTemplates": "",
"factoryTemplates": "",
"controllerTemplates": "",
"decoratorTemplates": "",
"providerTemplates": "",
"routeTemplates": ""
}
}

697
Gruntfile.js Normal file
View File

@ -0,0 +1,697 @@
// Generated on 2015-02-05 using generator-angular-fullstack 2.0.13
'use strict';
module.exports = function (grunt) {
var localConfig;
try {
localConfig = require('./server/config/local.env');
} catch(e) {
localConfig = {};
}
// Load grunt tasks automatically, when needed
require('jit-grunt')(grunt, {
express: 'grunt-express-server',
useminPrepare: 'grunt-usemin',
ngtemplates: 'grunt-angular-templates',
cdnify: 'grunt-google-cdn',
protractor: 'grunt-protractor-runner',
injector: 'grunt-asset-injector',
buildcontrol: 'grunt-build-control'
});
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// Define the configuration for all the tasks
grunt.initConfig({
// Project settings
pkg: grunt.file.readJSON('package.json'),
yeoman: {
// configurable paths
client: require('./bower.json').appPath || 'client',
dist: 'dist'
},
express: {
options: {
port: process.env.PORT || 80
},
dev: {
options: {
script: 'server/app.js',
debug: true
}
},
prod: {
options: {
script: 'dist/server/app.js'
}
}
},
open: {
server: {
url: 'http://localhost:<%= express.options.port %>'
}
},
watch: {
injectJS: {
files: [
'<%= yeoman.client %>/{app,components}/**/*.js',
'!<%= yeoman.client %>/{app,components}/**/*.spec.js',
'!<%= yeoman.client %>/{app,components}/**/*.mock.js',
'!<%= yeoman.client %>/app/app.js'],
tasks: ['injector:scripts']
},
injectCss: {
files: [
'<%= yeoman.client %>/{app,components}/**/*.css'
],
tasks: ['injector:css']
},
mochaTest: {
files: ['server/**/*.spec.js'],
tasks: ['env:test', 'mochaTest']
},
jsTest: {
files: [
'<%= yeoman.client %>/{app,components}/**/*.spec.js',
'<%= yeoman.client %>/{app,components}/**/*.mock.js'
],
tasks: ['newer:jshint:all', 'karma']
},
injectSass: {
files: [
'<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'],
tasks: ['injector:sass']
},
sass: {
files: [
'<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'],
tasks: ['sass', 'autoprefixer']
},
jade: {
files: [
'<%= yeoman.client %>/{app,components}/*',
'<%= yeoman.client %>/{app,components}/**/*.jade'],
tasks: ['jade']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
files: [
'{.tmp,<%= yeoman.client %>}/{app,components}/**/*.css',
'{.tmp,<%= yeoman.client %>}/{app,components}/**/*.html',
'{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',
'!{.tmp,<%= yeoman.client %>}{app,components}/**/*.spec.js',
'!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js',
'<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}'
],
options: {
livereload: true
}
},
express: {
files: [
'server/**/*.{js,json}'
],
tasks: ['express:dev', 'wait'],
options: {
livereload: true,
nospawn: true //Without this option specified express won't be reloaded
}
}
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: '<%= yeoman.client %>/.jshintrc',
reporter: require('jshint-stylish')
},
server: {
options: {
jshintrc: 'server/.jshintrc'
},
src: [
'server/**/*.js',
'!server/**/*.spec.js'
]
},
serverTest: {
options: {
jshintrc: 'server/.jshintrc-spec'
},
src: ['server/**/*.spec.js']
},
all: [
'<%= yeoman.client %>/{app,components}/**/*.js',
'!<%= yeoman.client %>/{app,components}/**/*.spec.js',
'!<%= yeoman.client %>/{app,components}/**/*.mock.js'
],
test: {
src: [
'<%= yeoman.client %>/{app,components}/**/*.spec.js',
'<%= yeoman.client %>/{app,components}/**/*.mock.js'
]
}
},
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/*',
'!<%= yeoman.dist %>/.git*',
'!<%= yeoman.dist %>/.openshift',
'!<%= yeoman.dist %>/Procfile'
]
}]
},
server: '.tmp'
},
// Add vendor prefixed styles
autoprefixer: {
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
cwd: '.tmp/',
src: '{,*/}*.css',
dest: '.tmp/'
}]
}
},
// Debugging with node inspector
'node-inspector': {
custom: {
options: {
'web-host': 'localhost'
}
}
},
// Use nodemon to run server in debug mode with an initial breakpoint
nodemon: {
debug: {
script: 'server/app.js',
options: {
nodeArgs: ['--debug-brk'],
env: {
PORT: process.env.PORT || 80
},
callback: function (nodemon) {
nodemon.on('log', function (event) {
console.log(event.colour);
});
// opens browser on initial server start
nodemon.on('config:update', function () {
setTimeout(function () {
require('open')('http://localhost:8080/debug?port=5858');
}, 500);
});
}
}
}
},
// Automatically inject Bower components into the app
wiredep: {
target: {
src: '<%= yeoman.client %>/index.html',
ignorePath: '<%= yeoman.client %>/',
exclude: [/bootstrap-sass-official/, /bootstrap.js/, '/json3/', '/es5-shim/', /bootstrap.css/, /font-awesome.css/ ]
}
},
// Renames files for browser caching purposes
rev: {
dist: {
files: {
src: [
'<%= yeoman.dist %>/public/{,*/}*.js',
'<%= yeoman.dist %>/public/{,*/}*.css',
'<%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/public/assets/fonts/*'
]
}
}
},
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
html: ['<%= yeoman.client %>/index.html'],
options: {
dest: '<%= yeoman.dist %>/public'
}
},
// Performs rewrites based on rev and the useminPrepare configuration
usemin: {
html: ['<%= yeoman.dist %>/public/{,*/}*.html'],
css: ['<%= yeoman.dist %>/public/{,*/}*.css'],
js: ['<%= yeoman.dist %>/public/{,*/}*.js'],
options: {
assetsDirs: [
'<%= yeoman.dist %>/public',
'<%= yeoman.dist %>/public/assets/images'
],
// This is so we update image references in our ng-templates
patterns: {
js: [
[/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images']
]
}
}
},
// The following *-min tasks produce minified files in the dist folder
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.client %>/assets/images',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: '<%= yeoman.dist %>/public/assets/images'
}]
}
},
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.client %>/assets/images',
src: '{,*/}*.svg',
dest: '<%= yeoman.dist %>/public/assets/images'
}]
}
},
// Allow the use of non-minsafe AngularJS files. Automatically makes it
// minsafe compatible so Uglify does not destroy the ng references
ngAnnotate: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat',
src: '*/**.js',
dest: '.tmp/concat'
}]
}
},
// Package all the html partials into a single javascript payload
ngtemplates: {
options: {
// This should be the name of your apps angular module
module: 'markdownFormatWdiffApp',
htmlmin: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true
},
usemin: 'app/app.js'
},
main: {
cwd: '<%= yeoman.client %>',
src: ['{app,components}/**/*.html'],
dest: '.tmp/templates.js'
},
tmp: {
cwd: '.tmp',
src: ['{app,components}/**/*.html'],
dest: '.tmp/tmp-templates.js'
}
},
// Replace Google CDN references
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/public/*.html']
}
},
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.client %>',
dest: '<%= yeoman.dist %>/public',
src: [
'*.{ico,png,txt}',
'.htaccess',
'bower_components/**/*',
'assets/images/{,*/}*.{webp}',
'assets/fonts/**/*',
'index.html'
]
}, {
expand: true,
cwd: '.tmp/images',
dest: '<%= yeoman.dist %>/public/assets/images',
src: ['generated/*']
}, {
expand: true,
dest: '<%= yeoman.dist %>',
src: [
'package.json',
'server/**/*'
]
}]
},
styles: {
expand: true,
cwd: '<%= yeoman.client %>',
dest: '.tmp/',
src: ['{app,components}/**/*.css']
}
},
buildcontrol: {
options: {
dir: 'dist',
commit: true,
push: true,
connectCommits: false,
message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
},
heroku: {
options: {
remote: 'heroku',
branch: 'master'
}
},
openshift: {
options: {
remote: 'openshift',
branch: 'master'
}
}
},
// Run some tasks in parallel to speed up the build process
concurrent: {
server: [
'jade',
'sass',
],
test: [
'jade',
'sass',
],
debug: {
tasks: [
'nodemon',
'node-inspector'
],
options: {
logConcurrentOutput: true
}
},
dist: [
'jade',
'sass',
//'imagemin',
'svgmin'
]
},
// Test settings
karma: {
unit: {
configFile: 'karma.conf.js',
singleRun: true
}
},
mochaTest: {
options: {
reporter: 'spec'
},
src: ['server/**/*.spec.js']
},
protractor: {
options: {
configFile: 'protractor.conf.js'
},
chrome: {
options: {
args: {
browser: 'chrome'
}
}
}
},
env: {
test: {
NODE_ENV: 'test'
},
prod: {
NODE_ENV: 'production'
},
all: localConfig
},
// Compiles Jade to html
jade: {
compile: {
options: {
data: {
debug: false
}
},
files: [{
expand: true,
cwd: '<%= yeoman.client %>',
src: [
'{app,components}/**/*.jade'
],
dest: '.tmp',
ext: '.html'
}]
}
},
// Compiles Sass to CSS
sass: {
server: {
options: {
loadPath: [
'<%= yeoman.client %>/bower_components',
'<%= yeoman.client %>/app',
'<%= yeoman.client %>/components'
],
compass:false
},
files: {
'.tmp/app/app.css' : '<%= yeoman.client %>/app/app.scss'
}
}
},
injector: {
options: {
},
// Inject application script files into index.html (doesn't include bower)
scripts: {
options: {
transform: function(filePath) {
filePath = filePath.replace('/client/', '');
filePath = filePath.replace('/.tmp/', '');
return '<script src="' + filePath + '"></script>';
},
starttag: '<!-- injector:js -->',
endtag: '<!-- endinjector -->'
},
files: {
'<%= yeoman.client %>/index.html': [
['{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',
'!{.tmp,<%= yeoman.client %>}/app/app.js',
'!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js',
'!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js']
]
}
},
// Inject component scss into app.scss
sass: {
options: {
transform: function(filePath) {
filePath = filePath.replace('/client/app/', '');
filePath = filePath.replace('/client/components/', '');
return '@import \'' + filePath + '\';';
},
starttag: '// injector',
endtag: '// endinjector'
},
files: {
'<%= yeoman.client %>/app/app.scss': [
'<%= yeoman.client %>/{app,components}/**/*.{scss,sass}',
'!<%= yeoman.client %>/app/app.{scss,sass}'
]
}
},
// Inject component css into index.html
css: {
options: {
transform: function(filePath) {
filePath = filePath.replace('/client/', '');
filePath = filePath.replace('/.tmp/', '');
return '<link rel="stylesheet" href="' + filePath + '">';
},
starttag: '<!-- injector:css -->',
endtag: '<!-- endinjector -->'
},
files: {
'<%= yeoman.client %>/index.html': [
'<%= yeoman.client %>/{app,components}/**/*.css'
]
}
}
},
});
// Used for delaying livereload until after server has restarted
grunt.registerTask('wait', function () {
grunt.log.ok('Waiting for server reload...');
var done = this.async();
setTimeout(function () {
grunt.log.writeln('Done waiting!');
done();
}, 1500);
});
grunt.registerTask('express-keepalive', 'Keep grunt running', function() {
this.async();
});
grunt.registerTask('serve', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'express-keepalive']);
}
if (target === 'debug') {
return grunt.task.run([
'clean:server',
'env:all',
'injector:sass',
'concurrent:server',
'injector',
'wiredep',
'autoprefixer',
'concurrent:debug'
]);
}
grunt.task.run([
'clean:server',
'env:all',
'injector:sass',
'concurrent:server',
'injector',
'wiredep',
'autoprefixer',
'express:dev',
'wait',
'watch'
]);
});
grunt.registerTask('server', function () {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run(['serve']);
});
grunt.registerTask('test', function(target) {
if (target === 'server') {
return grunt.task.run([
'env:all',
'env:test',
'mochaTest'
]);
}
else if (target === 'client') {
return grunt.task.run([
'clean:server',
'env:all',
'injector:sass',
'concurrent:test',
'injector',
'autoprefixer',
'karma'
]);
}
else if (target === 'e2e') {
return grunt.task.run([
'clean:server',
'env:all',
'env:test',
'injector:sass',
'concurrent:test',
'injector',
'wiredep',
'autoprefixer',
'express:dev',
'protractor'
]);
}
else grunt.task.run([
'test:server',
'test:client'
]);
});
grunt.registerTask('build', [
'clean:dist',
'injector:sass',
'concurrent:dist',
'injector',
'wiredep',
'useminPrepare',
'autoprefixer',
'ngtemplates',
'concat',
'ngAnnotate',
'copy:dist',
'cdnify',
'cssmin',
'uglify',
'rev',
'usemin'
]);
grunt.registerTask('default', [
'newer:jshint',
'test',
'build'
]);
};

View File

@ -1,74 +1,44 @@
# dubdiff
A diff viewer for markdown-formatted and plaintext documents.
A diff viewer for markdown-formatted documents.
These diffs are intended for use in copy-editing. The diffs are performed word-by-word, similarly to how the [GNU `wdiff`](http://www.gnu.org/software/wdiff/) tool works. This produces a more meaningful diff for English-language editing.
Uses the [`wdiff`](http://www.gnu.org/software/wdiff/) tool as a diffing engine. This produces an output that is more useful for copy-editing tasks. This wdiff comparison is then processed in a way that is aware of markdown formatting. The resulting output attempts to show differences of copy within the final document format (rather than differences of format).
The diff may be further processed in a way that is aware of markdown formatting. The resulting output attempts to show differences of copy within the final document format (rather than differences of format).
The markdown-sensitive processing of the wdiff comparison is at `...`, for the curious.
The markdown-sensitive processing of the wdiff comparison is at `server/components/wdiff/index.js`, for the curious.
## Version 2
This is a complete rewrite of Dubdiff with:
- simpler project architecture
- client-side diffing engine and simplified server
- server-side rendering
- switch to React from Angular
- clean up of diffing engine
- goal of implementing a HTML diff viewer
Basically I'm rewriting it for fun.
## Live Server
## Live Version
The tool is live at http://dubdiff.com, feel free to use it there.
## Provisioning
You'll need node & npm. Then install dependencies with
You'll need the following:
- node & npm
- grunt and bower (`npm install -g grunt bower`)
- ruby (`apt install ruby`)
- sass (`gem install sass`)
- wdiff (`apt install wdiff`)
npm install
To build and launch a dev server:
npm start
npm run server
To build and launch the production server:
npm run build:prod
npm run serve:prod
Data is saved to a simple flat file db in the `data` folder. If this folder doesn't exist, create it.
The wdiff binary should be placed in the `bin` subfolder, or a link should be made to the binary. Eg. `ln -s /usr/bin/wdiff bin/wdiff`.
npm install && bower install
mkdir data
### Low-memory environments
On a low-memory machine, eg. a DigitalOcean 512MB instance, you will need to enable virtual memory. Use this guide:
[How To Configure Virtual Memory (Swap File) on a VPS](https://www.digitalocean.com/community/tutorials/how-to-configure-virtual-memory-swap-file-on-a-vps#2)
### Start on boot
To make the application start on boot, run the following:
# initialize pm2 to start on boot with the systemd boot manager
pm2 start grunt --name dubdiff -- serve:dist
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
[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)

View File

@ -1 +0,0 @@
Support for displaying and responding to `#markdown` path suffix.

24
bower.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "markdown-format-wdiff",
"version": "0.0.0",
"dependencies": {
"angular": ">=1.2.*",
"json3": "~3.3.1",
"es5-shim": "~3.0.1",
"jquery": "~1.11.0",
"bootstrap-sass-official": "~3.1.1",
"bootstrap": "~3.1.1",
"angular-resource": ">=1.2.*",
"angular-cookies": ">=1.2.*",
"angular-sanitize": ">=1.2.*",
"angular-route": ">=1.2.*",
"angular-bootstrap": "~0.11.0",
"font-awesome": ">=4.1.0",
"lodash": "~2.4.1",
"angular-markdown-directive": "~0.3.1"
},
"devDependencies": {
"angular-mocks": ">=1.2.*",
"angular-scenario": ">=1.2.*"
}
}

543
client/.htaccess Normal file
View File

@ -0,0 +1,543 @@
# Apache Configuration File
# (!) Using `.htaccess` files slows down Apache, therefore, if you have access
# to the main server config file (usually called `httpd.conf`), you should add
# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html.
# ##############################################################################
# # CROSS-ORIGIN RESOURCE SHARING (CORS) #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Cross-domain AJAX requests |
# ------------------------------------------------------------------------------
# Enable cross-origin AJAX requests.
# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
# http://enable-cors.org/
# <IfModule mod_headers.c>
# Header set Access-Control-Allow-Origin "*"
# </IfModule>
# ------------------------------------------------------------------------------
# | CORS-enabled images |
# ------------------------------------------------------------------------------
# Send the CORS header for images when browsers request it.
# https://developer.mozilla.org/en/CORS_Enabled_Image
# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
<FilesMatch "\.(gif|ico|jpe?g|png|svg|svgz|webp)$">
SetEnvIf Origin ":" IS_CORS
Header set Access-Control-Allow-Origin "*" env=IS_CORS
</FilesMatch>
</IfModule>
</IfModule>
# ------------------------------------------------------------------------------
# | Web fonts access |
# ------------------------------------------------------------------------------
# Allow access from all domains for web fonts
<IfModule mod_headers.c>
<FilesMatch "\.(eot|font.css|otf|ttc|ttf|woff)$">
Header set Access-Control-Allow-Origin "*"
</FilesMatch>
</IfModule>
# ##############################################################################
# # ERRORS #
# ##############################################################################
# ------------------------------------------------------------------------------
# | 404 error prevention for non-existing redirected folders |
# ------------------------------------------------------------------------------
# Prevent Apache from returning a 404 error for a rewrite if a directory
# with the same name does not exist.
# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews
# http://www.webmasterworld.com/apache/3808792.htm
Options -MultiViews
# ------------------------------------------------------------------------------
# | Custom error messages / pages |
# ------------------------------------------------------------------------------
# You can customize what Apache returns to the client in case of an error (see
# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.:
ErrorDocument 404 /404.html
# ##############################################################################
# # INTERNET EXPLORER #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Better website experience |
# ------------------------------------------------------------------------------
# Force IE to render pages in the highest available mode in the various
# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf.
<IfModule mod_headers.c>
Header set X-UA-Compatible "IE=edge"
# `mod_headers` can't match based on the content-type, however, we only
# want to send this header for HTML pages and not for the other resources
<FilesMatch "\.(appcache|crx|css|eot|gif|htc|ico|jpe?g|js|m4a|m4v|manifest|mp4|oex|oga|ogg|ogv|otf|pdf|png|safariextz|svg|svgz|ttf|vcf|webapp|webm|webp|woff|xml|xpi)$">
Header unset X-UA-Compatible
</FilesMatch>
</IfModule>
# ------------------------------------------------------------------------------
# | Cookie setting from iframes |
# ------------------------------------------------------------------------------
# Allow cookies to be set from iframes in IE.
# <IfModule mod_headers.c>
# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
# </IfModule>
# ------------------------------------------------------------------------------
# | Screen flicker |
# ------------------------------------------------------------------------------
# Stop screen flicker in IE on CSS rollovers (this only works in
# combination with the `ExpiresByType` directives for images from below).
# BrowserMatch "MSIE" brokenvary=1
# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1
# BrowserMatch "Opera" !brokenvary
# SetEnvIf brokenvary 1 force-no-vary
# ##############################################################################
# # MIME TYPES AND ENCODING #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Proper MIME types for all files |
# ------------------------------------------------------------------------------
<IfModule mod_mime.c>
# Audio
AddType audio/mp4 m4a f4a f4b
AddType audio/ogg oga ogg
# JavaScript
# Normalize to standard type (it's sniffed in IE anyways):
# http://tools.ietf.org/html/rfc4329#section-7.2
AddType application/javascript js jsonp
AddType application/json json
# Video
AddType video/mp4 mp4 m4v f4v f4p
AddType video/ogg ogv
AddType video/webm webm
AddType video/x-flv flv
# Web fonts
AddType application/font-woff woff
AddType application/vnd.ms-fontobject eot
# Browsers usually ignore the font MIME types and sniff the content,
# however, Chrome shows a warning if other MIME types are used for the
# following fonts.
AddType application/x-font-ttf ttc ttf
AddType font/opentype otf
# Make SVGZ fonts work on iPad:
# https://twitter.com/FontSquirrel/status/14855840545
AddType image/svg+xml svg svgz
AddEncoding gzip svgz
# Other
AddType application/octet-stream safariextz
AddType application/x-chrome-extension crx
AddType application/x-opera-extension oex
AddType application/x-shockwave-flash swf
AddType application/x-web-app-manifest+json webapp
AddType application/x-xpinstall xpi
AddType application/xml atom rdf rss xml
AddType image/webp webp
AddType image/x-icon ico
AddType text/cache-manifest appcache manifest
AddType text/vtt vtt
AddType text/x-component htc
AddType text/x-vcard vcf
</IfModule>
# ------------------------------------------------------------------------------
# | UTF-8 encoding |
# ------------------------------------------------------------------------------
# Use UTF-8 encoding for anything served as `text/html` or `text/plain`.
AddDefaultCharset utf-8
# Force UTF-8 for certain file formats.
<IfModule mod_mime.c>
AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml
</IfModule>
# ##############################################################################
# # URL REWRITES #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Rewrite engine |
# ------------------------------------------------------------------------------
# Turning on the rewrite engine and enabling the `FollowSymLinks` option is
# necessary for the following directives to work.
# If your web host doesn't allow the `FollowSymlinks` option, you may need to
# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the
# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
# Also, some cloud hosting services require `RewriteBase` to be set:
# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site
<IfModule mod_rewrite.c>
Options +FollowSymlinks
# Options +SymLinksIfOwnerMatch
RewriteEngine On
# RewriteBase /
</IfModule>
# ------------------------------------------------------------------------------
# | Suppressing / Forcing the "www." at the beginning of URLs |
# ------------------------------------------------------------------------------
# The same content should never be available under two different URLs especially
# not with and without "www." at the beginning. This can cause SEO problems
# (duplicate content), therefore, you should choose one of the alternatives and
# redirect the other one.
# By default option 1 (no "www.") is activated:
# http://no-www.org/faq.php?q=class_b
# If you'd prefer to use option 2, just comment out all the lines from option 1
# and uncomment the ones from option 2.
# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME!
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Option 1: rewrite www.example.com → example.com
<IfModule mod_rewrite.c>
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
</IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Option 2: rewrite example.com → www.example.com
# Be aware that the following might not be a good idea if you use "real"
# subdomains for certain parts of your website.
# <IfModule mod_rewrite.c>
# RewriteCond %{HTTPS} !=on
# RewriteCond %{HTTP_HOST} !^www\..+$ [NC]
# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# </IfModule>
# ##############################################################################
# # SECURITY #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Content Security Policy (CSP) |
# ------------------------------------------------------------------------------
# You can mitigate the risk of cross-site scripting and other content-injection
# attacks by setting a Content Security Policy which whitelists trusted sources
# of content for your site.
# The example header below allows ONLY scripts that are loaded from the current
# site's origin (no inline scripts, no CDN, etc). This almost certainly won't
# work as-is for your site!
# To get all the details you'll need to craft a reasonable policy for your site,
# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or
# see the specification: http://w3.org/TR/CSP).
# <IfModule mod_headers.c>
# Header set Content-Security-Policy "script-src 'self'; object-src 'self'"
# <FilesMatch "\.(appcache|crx|css|eot|gif|htc|ico|jpe?g|js|m4a|m4v|manifest|mp4|oex|oga|ogg|ogv|otf|pdf|png|safariextz|svg|svgz|ttf|vcf|webapp|webm|webp|woff|xml|xpi)$">
# Header unset Content-Security-Policy
# </FilesMatch>
# </IfModule>
# ------------------------------------------------------------------------------
# | File access |
# ------------------------------------------------------------------------------
# Block access to directories without a default document.
# Usually you should leave this uncommented because you shouldn't allow anyone
# to surf through every directory on your server (which may includes rather
# private places like the CMS's directories).
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Block access to hidden files and directories.
# This includes directories used by version control systems such as Git and SVN.
<IfModule mod_rewrite.c>
RewriteCond %{SCRIPT_FILENAME} -d [OR]
RewriteCond %{SCRIPT_FILENAME} -f
RewriteRule "(^|/)\." - [F]
</IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Block access to backup and source files.
# These files may be left by some text editors and can pose a great security
# danger when anyone has access to them.
<FilesMatch "(^#.*#|\.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$">
Order allow,deny
Deny from all
Satisfy All
</FilesMatch>
# ------------------------------------------------------------------------------
# | Secure Sockets Layer (SSL) |
# ------------------------------------------------------------------------------
# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.:
# prevent `https://www.example.com` when your certificate only allows
# `https://secure.example.com`.
# <IfModule mod_rewrite.c>
# RewriteCond %{SERVER_PORT} !^443
# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L]
# </IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Force client-side SSL redirection.
# If a user types "example.com" in his browser, the above rule will redirect him
# to the secure version of the site. That still leaves a window of opportunity
# (the initial HTTP connection) for an attacker to downgrade or redirect the
# request. The following header ensures that browser will ONLY connect to your
# server via HTTPS, regardless of what the users type in the address bar.
# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/
# <IfModule mod_headers.c>
# Header set Strict-Transport-Security max-age=16070400;
# </IfModule>
# ------------------------------------------------------------------------------
# | Server software information |
# ------------------------------------------------------------------------------
# Avoid displaying the exact Apache version number, the description of the
# generic OS-type and the information about Apache's compiled-in modules.
# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`!
# ServerTokens Prod
# ##############################################################################
# # WEB PERFORMANCE #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Compression |
# ------------------------------------------------------------------------------
<IfModule mod_deflate.c>
# Force compression for mangled headers.
# http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
# Compress all output labeled with one of the following MIME-types
# (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
# and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines
# as `AddOutputFilterByType` is still in the core directives).
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE application/atom+xml \
application/javascript \
application/json \
application/rss+xml \
application/vnd.ms-fontobject \
application/x-font-ttf \
application/x-web-app-manifest+json \
application/xhtml+xml \
application/xml \
font/opentype \
image/svg+xml \
image/x-icon \
text/css \
text/html \
text/plain \
text/x-component \
text/xml
</IfModule>
</IfModule>
# ------------------------------------------------------------------------------
# | Content transformations |
# ------------------------------------------------------------------------------
# Prevent some of the mobile network providers from modifying the content of
# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5.
# <IfModule mod_headers.c>
# Header set Cache-Control "no-transform"
# </IfModule>
# ------------------------------------------------------------------------------
# | ETag removal |
# ------------------------------------------------------------------------------
# Since we're sending far-future expires headers (see below), ETags can
# be removed: http://developer.yahoo.com/performance/rules.html#etags.
# `FileETag None` is not enough for every server.
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
# ------------------------------------------------------------------------------
# | Expires headers (for better cache control) |
# ------------------------------------------------------------------------------
# The following expires headers are set pretty far in the future. If you don't
# control versioning with filename-based cache busting, consider lowering the
# cache time for resources like CSS and JS to something like 1 week.
<IfModule mod_expires.c>
ExpiresActive on
ExpiresDefault "access plus 1 month"
# CSS
ExpiresByType text/css "access plus 1 year"
# Data interchange
ExpiresByType application/json "access plus 0 seconds"
ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType text/xml "access plus 0 seconds"
# Favicon (cannot be renamed!)
ExpiresByType image/x-icon "access plus 1 week"
# HTML components (HTCs)
ExpiresByType text/x-component "access plus 1 month"
# HTML
ExpiresByType text/html "access plus 0 seconds"
# JavaScript
ExpiresByType application/javascript "access plus 1 year"
# Manifest files
ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
ExpiresByType text/cache-manifest "access plus 0 seconds"
# Media
ExpiresByType audio/ogg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType video/mp4 "access plus 1 month"
ExpiresByType video/ogg "access plus 1 month"
ExpiresByType video/webm "access plus 1 month"
# Web feeds
ExpiresByType application/atom+xml "access plus 1 hour"
ExpiresByType application/rss+xml "access plus 1 hour"
# Web fonts
ExpiresByType application/font-woff "access plus 1 month"
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
ExpiresByType application/x-font-ttf "access plus 1 month"
ExpiresByType font/opentype "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
</IfModule>
# ------------------------------------------------------------------------------
# | Filename-based cache busting |
# ------------------------------------------------------------------------------
# If you're not using a build process to manage your filename version revving,
# you might want to consider enabling the following directives to route all
# requests such as `/css/style.12345.css` to `/css/style.css`.
# To understand why this is important and a better idea than `*.css?v231`, read:
# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring
# <IfModule mod_rewrite.c>
# RewriteCond %{REQUEST_FILENAME} !-f
# RewriteCond %{REQUEST_FILENAME} !-d
# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
# </IfModule>
# ------------------------------------------------------------------------------
# | File concatenation |
# ------------------------------------------------------------------------------
# Allow concatenation from within specific CSS and JS files, e.g.:
# Inside of `script.combined.js` you could have
# <!--#include file="libs/jquery.js" -->
# <!--#include file="plugins/jquery.idletimer.js" -->
# and they would be included into this single file.
# <IfModule mod_include.c>
# <FilesMatch "\.combined\.js$">
# Options +Includes
# AddOutputFilterByType INCLUDES application/javascript application/json
# SetOutputFilter INCLUDES
# </FilesMatch>
# <FilesMatch "\.combined\.css$">
# Options +Includes
# AddOutputFilterByType INCLUDES text/css
# SetOutputFilter INCLUDES
# </FilesMatch>
# </IfModule>
# ------------------------------------------------------------------------------
# | Persistent connections |
# ------------------------------------------------------------------------------
# Allow multiple requests to be sent over the same TCP connection:
# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive.
# Enable if you serve a lot of static content but, be aware of the
# possible disadvantages!
# <IfModule mod_headers.c>
# Header set Connection Keep-Alive
# </IfModule>

38
client/.jshintrc Normal file
View File

@ -0,0 +1,38 @@
{
"node": true,
"browser": true,
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"smarttabs": true,
"globals": {
"jQuery": true,
"angular": true,
"console": true,
"$": true,
"_": true,
"moment": true,
"describe": true,
"beforeEach": true,
"module": true,
"inject": true,
"it": true,
"expect": true,
"browser": true,
"element": true,
"by": true
}
}

18
client/app/app.js Normal file
View File

@ -0,0 +1,18 @@
'use strict';
angular.module('markdownFormatWdiffApp', [
'ngCookies',
'ngResource',
'ngSanitize',
'ngRoute',
'ui.bootstrap',
'btford.markdown'
])
.config(function ($routeProvider, $locationProvider, $httpProvider) {
$routeProvider
.otherwise ({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
})

55
client/app/app.scss Normal file
View File

@ -0,0 +1,55 @@
$icon-font-path: "/bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/";
$fa-font-path: "/bower_components/font-awesome/fonts";
@import 'bootstrap-sass-official/vendor/assets/stylesheets/bootstrap';
@import 'font-awesome/scss/font-awesome';
/**
* App-wide Styles
*/
.hero-unit {
margin: 20px;
}
.hero-unit h1 a {
color: #ffffff;
}
.browsehappy {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
.indent {
margin-left: 2em;
}
table.revisions th, table.revisions td{
padding-right: 1em;
}
table.revisions td.content {
max-height: 30em;
}
table.revisions .state {
width: 7em;
}
form.controls {
margin-top: 1em;
}
form.form-inline.controls > * {
margin-right: 1em;
}
form.form-inline.controls > .form-group > label, {
margin-right: 0.5em;
}
// Component styles are injected through grunt
// injector
@import 'compare/compare.scss';
@import 'modal/modal.scss';
// endinjector

View File

@ -0,0 +1,18 @@
'use strict';
angular.module('markdownFormatWdiffApp')
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'app/compare/create/create.html',
controller: 'CompareCreateCtrl'
})
.when('/:id', {
templateUrl: 'app/compare/show/show.html',
controller: 'CompareShowCtrl'
})
.when('/:id/:format', {
templateUrl: 'app/compare/show/show.html',
controller: 'CompareShowCtrl'
});
});

View File

@ -0,0 +1,57 @@
.wdiff ins {
background-color: #dbffdb;
}
.wdiff del {
background-color: #f8cbcb;
}
.content-pre {
font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif;
margin-bottom: 10px;
padding: 5px;
width: auto;
white-space: pre-wrap;
}
.content-well {
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
//background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
}
#docA, #docB {
resize: vertical;
min-height: 600px;
}
#banner {
border-bottom: none;
margin-top: -20px;
}
#banner h1 {
font-size: 60px;
line-height: 1;
letter-spacing: -1px;
}
.hero-unit {
position: relative;
padding: 30px 15px;
color: #F5F5F5;
text-align: center;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
background: #4393B9;
}
.footer {
text-align: center;
padding: 30px 0;
margin-top: 70px;
border-top: 1px solid #E5E5E5;
}

View File

@ -0,0 +1,32 @@
'use strict';
angular.module('markdownFormatWdiffApp')
.controller('CompareCreateCtrl', function ($scope, $http, $location) {
$scope.docA = "";
$scope.docB = "";
$scope.wdiff = "";
$scope.wdiffMarkdown = "";
$scope.isMarkdownFormat = true;
$scope.compare = function() {
$http.post('/api/compare',
{ a: _.escape($scope.docA), b: _.escape($scope.docB) },
{headers:{"Content-Type":"application/json"}})
.success(function (comparison) {
$location.path('/'+comparison._id);
$location.hash($scope.isMarkdownFormat?'markdown':'plaintext');
});
};
$scope.toggleMarkdownFormat = function() {
if ($scope.isMarkdownFormat) {
$scope.isMarkdownFormat = false;
}
else {
$scope.isMarkdownFormat = true;
}
}
})

View File

@ -0,0 +1,30 @@
// nav(ng-include='"components/navbar/navbar.html"')
nav(ng-include='"components/elements/header.html"', onload='title = "dubdiff"; ')
.container
form.row
.col-md-2.col-sm-12.form-group
.controls.well.col-lg-12
a.btn.btn-block.btn-primary(type='button', ng-click='compare()') compare
.controls.well.btn-group.col-lg-12
a.btn.btn-block.btn-primary(ng-class='{"active": isMarkdownFormat}', type='submit', ng-click='toggleMarkdownFormat()')
span.glyphicon(ng-class='{"glyphicon-ok": isMarkdownFormat}')
| &nbsp; As Markdown
.col-lg-5.col-sm-12.form-group
label(for='docA')
| Original
textarea.form-control(id='docA', ng-model='docA')
.col-lg-5.col-sm-12.form-group
label(for='docB')
| Final
textarea.form-control(id='docB', ng-model='docB')
footer(ng-include='"components/elements/footer.html"')

View File

@ -0,0 +1,57 @@
'use strict';
var MARKDOWN = "markdown";
var PLAINTEXT = "plaintext";
angular.module('markdownFormatWdiffApp')
.controller('CompareShowCtrl', function ($scope, $routeParams, $http, $location) {
$scope.wdiff = '';
$scope.before = '';
$scope.after = '';
$scope.isShowWdiff = true;
$scope.isMarkdownFormat = true;
var paramFormat = $location.hash();
if (paramFormat == "plain" || paramFormat == "plaintext")
$scope.isMarkdownFormat = false;
// if routeParams specifies a user, restrict the query to that user
var path = '/api/compare/wdiff/' + $routeParams.id;
$http.get(path).success(function(comparison) {
$scope.wdiff = comparison.wdiff;
$scope.before = comparison.a;
$scope.after = comparison.b;
});
$scope.showBefore = function() {
$scope.isShowBefore = true;
$scope.isShowAfter = false;
$scope.isShowWdiff = false;
}
$scope.showAfter = function() {
$scope.isShowBefore = false;
$scope.isShowAfter = true;
$scope.isShowWdiff = false;
}
$scope.showWdiff = function() {
$scope.isShowBefore = false;
$scope.isShowAfter = false;
$scope.isShowWdiff = true;
}
$scope.toggleMarkdownFormat = function() {
if ($scope.isMarkdownFormat) {
$scope.isMarkdownFormat = false;
$location.hash('plaintext');
$location.replace();
}
else {
$scope.isMarkdownFormat = true;
$location.hash('markdown');
$location.replace();
}
}
})

View File

@ -0,0 +1,46 @@
// nav(ng-include='"components/navbar/navbar.html"')
nav(ng-include='"components/elements/header.html"', onload='title = "dubdiff"; subtitle ="results"')
.container
.row
.col-md-2.col-sm-12
.controls.well.btn-group.col-lg-12
a.btn.btn-block.btn-primary(ng-class='{"active": isShowBefore}', type='submit', ng-click='showBefore()')
| Original
a.btn.btn-block.btn-primary(ng-class='{"active": isShowAfter}', type='submit', ng-click='showAfter()')
| Final
a.btn.btn-block.btn-primary(ng-class='{"active": isShowWdiff}', type='submit', ng-click='showWdiff()')
| Difference
.controls.well.btn-group.col-lg-12
a.btn.btn-block.btn-primary(ng-class='{"active": isMarkdownFormat}', type='submit', ng-click='toggleMarkdownFormat()')
span.glyphicon(ng-class='{"glyphicon-ok": isMarkdownFormat}')
| &nbsp; As Markdown
div(ng-if='isMarkdownFormat')
.col-md-10.col-sm-12.content-well(ng-show='isShowBefore')
div.before(btf-markdown='before')
.col-md-10.col-sm-12.content-well(ng-show='isShowWdiff')
div.wdiff(btf-markdown='wdiff')
.col-md-10.col-sm-12.content-well(ng-show='isShowAfter')
div.after(btf-markdown='after')
div(ng-if='!isMarkdownFormat')
.col-md-10.col-sm-12.content-well(ng-show='isShowBefore')
.content-pre.before(ng-bind-html='before')
.col-md-10.col-sm-12.content-well(ng-show='isShowWdiff')
.content-pre.wdiff(ng-bind-html='wdiff')
.col-md-10.col-sm-12.content-well(ng-show='isShowAfter')
.content-pre.after(ng-bind-html='after')
footer(ng-include='"components/elements/footer.html"')

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,8 @@
div.footer
.container
p
a(href='https://adamarthurryan.com') Adam Brown
= ' | '
= 'This website is '
a(href='https://github.com/adamarthurryan/dubdiff') open source
| .

View File

@ -0,0 +1,6 @@
header#banner.hero-unit
.container
h1
a(href='/')
| {{title}}
h3 {{subtitle}}

View File

@ -0,0 +1,8 @@
.modal-header
button.close(ng-if='modal.dismissable', type='button', ng-click='$dismiss()') &times;
h4.modal-title(ng-if='modal.title', ng-bind='modal.title')
.modal-body
p(ng-if='modal.text', ng-bind='modal.text')
div(ng-if='modal.html', ng-bind-html='modal.html')
.modal-footer
button.btn(ng-repeat='button in modal.buttons', ng-class='button.classes', ng-click='button.click($event)', ng-bind='button.text')

View File

@ -0,0 +1,25 @@
.modal-primary,
.modal-info,
.modal-success,
.modal-warning,
.modal-danger {
.modal-header {
color: #fff;
border-radius: 5px 5px 0 0;
}
}
.modal-primary .modal-header {
background: $brand-primary;
}
.modal-info .modal-header {
background: $brand-info;
}
.modal-success .modal-header {
background: $brand-success;
}
.modal-warning .modal-header {
background: $brand-warning;
}
.modal-danger .modal-header {
background: $brand-danger;
}

View File

@ -0,0 +1,77 @@
'use strict';
angular.module('markdownFormatWdiffApp')
.factory('Modal', function ($rootScope, $modal) {
/**
* Opens a modal
* @param {Object} scope - an object to be merged with modal's scope
* @param {String} modalClass - (optional) class(es) to be applied to the modal
* @return {Object} - the instance $modal.open() returns
*/
function openModal(scope, modalClass) {
var modalScope = $rootScope.$new();
scope = scope || {};
modalClass = modalClass || 'modal-default';
angular.extend(modalScope, scope);
return $modal.open({
templateUrl: 'components/modal/modal.html',
windowClass: modalClass,
scope: modalScope
});
}
// Public API here
return {
/* Confirmation modals */
confirm: {
/**
* Create a function to open a delete confirmation modal (ex. ng-click='myModalFn(name, arg1, arg2...)')
* @param {Function} del - callback, ran when delete is confirmed
* @return {Function} - the function to open the modal (ex. myModalFn)
*/
delete: function(del) {
del = del || angular.noop;
/**
* Open a delete confirmation modal
* @param {String} name - name or info to show on modal
* @param {All} - any additional args are passed staight to del callback
*/
return function() {
var args = Array.prototype.slice.call(arguments),
name = args.shift(),
deleteModal;
deleteModal = openModal({
modal: {
dismissable: true,
title: 'Confirm Delete',
html: '<p>Are you sure you want to delete <strong>' + name + '</strong> ?</p>',
buttons: [{
classes: 'btn-danger',
text: 'Delete',
click: function(e) {
deleteModal.close(e);
}
}, {
classes: 'btn-default',
text: 'Cancel',
click: function(e) {
deleteModal.dismiss(e);
}
}]
}
}, 'modal-danger');
deleteModal.result.then(function(event) {
del.apply(event, args);
});
};
}
}
};
});

View File

@ -0,0 +1,17 @@
'use strict';
/**
* Removes server error when user updates input
*/
angular.module('markdownFormatWdiffApp')
.directive('mongooseError', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
element.on('keydown', function() {
return ngModel.$setValidity('mongoose', true);
});
}
};
});

View File

@ -0,0 +1,16 @@
'use strict';
angular.module('markdownFormatWdiffApp')
.controller('NavbarCtrl', function ($scope, $location) {
$scope.menu = [{
'title': 'Home',
'link': '/'
}];
$scope.isCollapsed = true;
$scope.isActive = function(route) {
return route === $location.path();
};
});

View File

@ -0,0 +1,34 @@
div.navbar.navbar-default.navbar-static-top(ng-controller='NavbarCtrl')
div.container
div.navbar-header
button.navbar-toggle(type='button', ng-click='isCollapsed = !isCollapsed')
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
a.navbar-brand(href='/') markdown-format-wdiff
div#navbar-main.navbar-collapse.collapse(collapse='isCollapsed')
ul.nav.navbar-nav
li(ng-repeat='item in menu', ng-class='{active: isActive(item.link)}')
a(ng-href='{{item.link}}') {{item.title}}
li(ng-show='isAdmin()', ng-class='{active: isActive("/admin")}')
a(href='/admin') Admin
ul.nav.navbar-nav.navbar-right
li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/signup")}')
a(href='/signup') Sign up
li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/login")}')
a(href='/login') Login
li(ng-show='isLoggedIn()')
p.navbar-text Hello {{ getCurrentUser().name }}
li(ng-show='isLoggedIn()', ng-class='{active: isActive("/settings")}')
a(href='/settings')
span.glyphicon.glyphicon-cog
li(ng-show='isLoggedIn()', ng-class='{active: isActive("/logout")}')
a(href='', ng-click='logout()') Logout

BIN
client/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

74
client/index.html Normal file
View File

@ -0,0 +1,74 @@
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css(client) app/vendor.css -->
<!-- bower:css -->
<!-- endbower -->
<!-- endbuild -->
<!-- build:css({.tmp,client}) app/app.css -->
<link rel="stylesheet" href="app/app.css">
<!-- injector:css -->
<!-- endinjector -->
<!-- endbuild -->
</head>
<body ng-app="markdownFormatWdiffApp">
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<!-- Add your site or application content here -->
<div ng-view=""></div>
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-X');
ga('send', 'pageview');
</script>
<!--[if lt IE 9]>
<script src="bower_components/es5-shim/es5-shim.js"></script>
<script src="bower_components/json3/lib/json3.min.js"></script>
<![endif]-->
<!-- build:js({client,node_modules}) app/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="bower_components/lodash/dist/lodash.compat.js"></script>
<script src="bower_components/showdown/src/showdown.js"></script>
<script src="bower_components/angular-markdown-directive/markdown.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,client}) app/app.js -->
<script src="app/app.js"></script>
<!-- injector:js -->
<script src="app/compare/compare.js"></script>
<script src="app/compare/create/create.controller.js"></script>
<script src="app/compare/show/show.controller.js"></script>
<script src="components/modal/modal.service.js"></script>
<script src="components/mongoose-error/mongoose-error.directive.js"></script>
<script src="components/navbar/navbar.controller.js"></script>
<!-- endinjector -->
<!-- endbuild -->
</body>
</html>

3
client/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# robotstxt.org
User-agent: *

BIN
dist/favicon-16x16.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
dist/favicon-32x32.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

BIN
dist/favicon-96x96.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
dist/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
dist/img/03-small.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

BIN
dist/img/03-tiny.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

BIN
dist/img/03-tinyer.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

11
dist/main.css vendored
View File

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

15
e2e/main/main.po.js Normal file
View File

@ -0,0 +1,15 @@
/**
* This file uses the Page Object pattern to define the main page for tests
* https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ
*/
'use strict';
var MainPage = function() {
this.heroEl = element(by.css('.hero-unit'));
this.h1El = this.heroEl.element(by.css('h1'));
this.imgEl = this.heroEl.element(by.css('img'));
};
module.exports = new MainPage();

16
e2e/main/main.spec.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
describe('Main View', function() {
var page;
beforeEach(function() {
browser.get('/');
page = require('./main.po');
});
it('should include jumbotron with correct data', function() {
expect(page.h1El.getText()).toBe('\'Allo, \'Allo!');
expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/);
expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman');
});
});

79
karma.conf.js Normal file
View File

@ -0,0 +1,79 @@
// Karma configuration
// http://karma-runner.github.io/0.10/config/configuration-file.html
module.exports = function(config) {
config.set({
// base path, that will be used to resolve files and exclude
basePath: '',
// testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'client/bower_components/jquery/dist/jquery.js',
'client/bower_components/angular/angular.js',
'client/bower_components/angular-mocks/angular-mocks.js',
'client/bower_components/angular-resource/angular-resource.js',
'client/bower_components/angular-cookies/angular-cookies.js',
'client/bower_components/angular-sanitize/angular-sanitize.js',
'client/bower_components/angular-route/angular-route.js',
'client/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
'client/bower_components/lodash/dist/lodash.compat.js',
'client/app/app.js',
'client/app/app.coffee',
'client/app/**/*.js',
'client/app/**/*.coffee',
'client/components/**/*.js',
'client/components/**/*.coffee',
'client/app/**/*.jade',
'client/components/**/*.jade',
'client/app/**/*.html',
'client/components/**/*.html'
],
preprocessors: {
'**/*.jade': 'ng-jade2js',
'**/*.html': 'html2js',
'**/*.coffee': 'coffee',
},
ngHtml2JsPreprocessor: {
stripPrefix: 'client/'
},
ngJade2JsPreprocessor: {
stripPrefix: 'client/'
},
// list of files / patterns to exclude
exclude: [],
// web server port
port: 8080,
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: false
});
};

View File

@ -0,0 +1,17 @@
{
"folders":
[
{
"follow_symlinks": true,
"path": "client"
},
{
"follow_symlinks": true,
"path": "e2e"
},
{
"follow_symlinks": true,
"path": "server"
}
]
}

View File

@ -0,0 +1,862 @@
{
"auto_complete":
{
"selected_items":
[
[
"wdif",
"wdiffMarkdown"
],
[
"selected",
"selectedProviders"
],
[
"btn",
"btn-primary"
],
[
"definition",
"definition"
],
[
"tran",
"translateLanguage"
],
[
"exp",
"expressionSearch"
],
[
"define",
"defineMeaning"
],
[
"colo",
"colorFactor"
],
[
"dup",
"duplicate_user"
],
[
"import_exter",
"import_external_images_nonce"
],
[
"page-break",
"page-break-inside"
],
[
"array_key",
"array_key_exists"
],
[
"guess",
"guess"
],
[
"category",
"category_list"
],
[
"posts_",
"posts_to_fix"
],
[
"in",
"inline-block"
],
[
"text-",
"text-decoration"
],
[
"get_ca",
"get_cat_id"
],
[
"flatten",
"flatten"
],
[
"con",
"contains"
],
[
"add",
"addClass"
],
[
"h",
"hidden"
],
[
"page",
"page-break-before"
]
]
},
"buffers":
[
{
"file": "client/index.html",
"settings":
{
"buffer_size": 3458,
"line_ending": "Windows"
}
},
{
"file": "bower.json",
"settings":
{
"buffer_size": 617,
"line_ending": "Unix"
}
},
{
"file": "client/app/app.js",
"settings":
{
"buffer_size": 349,
"line_ending": "Windows"
}
},
{
"file": "server/components/wdiff/index.js",
"settings":
{
"buffer_size": 8966,
"line_ending": "Unix"
}
},
{
"file": "/C/Users/Adam/Desktop/code/courses/git-status-all",
"settings":
{
"buffer_size": 113,
"line_ending": "Unix"
}
},
{
"file": "/C/Users/Adam/Downloads/JavaScript Refactoring - Course Notes.md",
"settings":
{
"buffer_size": 2388,
"line_ending": "Unix"
}
},
{
"file": "/C/Users/Adam/Dropbox (Envato)/Course Content/Code/ready/Create a New JavaScript Framework - How We Built the Daily Mail CMS/notes - QA.md",
"settings":
{
"buffer_size": 3928,
"line_ending": "Unix"
}
},
{
"file": "/C/Users/Adam/Dropbox (Envato)/Course Content/Code/ready/Create a New JavaScript Framework - Challenges of Rolling Your Own/notes - QA.md",
"settings":
{
"buffer_size": 4420,
"line_ending": "Unix"
}
}
],
"build_system": "",
"build_system_choices":
[
],
"build_varint": "",
"command_palette":
{
"height": 157.0,
"last_filter": "",
"selected_items":
[
[
"Package Control: ins",
"Package Control: Install Package"
],
[
"install",
"Package Control: Install Package"
],
[
"isntall",
"Package Control: Install Package"
],
[
"Package Control: in",
"Package Control: Install Package"
]
],
"width": 571.0
},
"console":
{
"height": 256.0,
"history":
[
"import urllib.request,os; pr='Preferences.sublime-settings'; ip='ignored_packages'; n='Package Control'; s=sublime.load_settings(pr); ig=s.get(ip); ig.append(n); s.set(ip,ig); sublime.save_settings('Preferences.sublime-settings'); pf=n+'.sublime-package'; urllib.request.install_opener(urllib.request.build_opener(urllib.request.ProxyHandler())); by=urllib.request.urlopen('https://packagecontrol.io/'+pf.replace(' ','%20')).read(); open(os.path.join(sublime.installed_packages_path(),pf),'wb').write(by); ig.remove(n); s.set(ip,ig); sublime.save_settings(pr); print('Package Control: 3.0.0 upgrade successful!')",
"import urllib.request,os,hashlib; h = '7183a2d3e96f11eeadd761d777e62404' + 'e330c659d4bb41d3bdf022e94cab3cd0'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try manual install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by)",
"Package Control: Install Package",
"import urllib.request,os,hashlib; h = '7183a2d3e96f11eeadd761d777e62404' + 'e330c659d4bb41d3bdf022e94cab3cd0'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try manual install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by)",
"alert(\"a\")",
"help"
]
},
"distraction_free":
{
"menu_visible": true,
"show_minimap": false,
"show_open_files": true,
"show_tabs": false,
"side_bar_visible": false,
"status_bar_visible": false
},
"expanded_folders":
[
"/C/Users/Adam/Desktop/code/projects/dubdiff/client",
"/C/Users/Adam/Desktop/code/projects/dubdiff/client/app",
"/C/Users/Adam/Desktop/code/projects/dubdiff/server",
"/C/Users/Adam/Desktop/code/projects/dubdiff/server/components",
"/C/Users/Adam/Desktop/code/projects/dubdiff/server/components/wdiff"
],
"file_history":
[
"/C/Users/Adam/Dropbox (Envato)/Course Content/Code/incoming/Create a New JavaScript Framework - Full Stack Reactivity with Milo/notes - edit.md",
"/C/Users/Adam/Desktop/code/courses/create-a-new-javascript-framework-full-stack-reactivity-with-milo/README.md",
"/C/Users/Adam/Desktop/code/projects/dubdiff/client/app/main/main.js",
"/C/Users/Adam/Desktop/code/projects/dubdiff/bin/markdown-format-wdiff",
"/C/Users/Adam/Desktop/code/projects/dubdiff/client/app/wdiff/wdiff.scss",
"/C/Users/Adam/Desktop/code/projects/dubdiff/client/app/wdiff/wdiff.jade",
"/C/Users/Adam/Desktop/code/projects/dubdiff/client/app/wdiff/wdiff.js",
"/C/Users/Adam/Desktop/code/projects/dubdiff/server/api/wdiff/wdiff.controller.js",
"/C/Users/Adam/Desktop/code/projects/dubdiff/server/api/wdiff/index.js",
"/C/Users/Adam/Desktop/code/projects/dubdiff/client/app/wdiff/wdiff.controller.js",
"/Z/lookup-lists/Gruntfile.js",
"/Z/docker/data/lookup-lists/client/app/list/list/list.jade",
"/Z/docker/data/lookup-lists/client/app/list/list/list.controller.js",
"/Z/docker/data/lookup-lists/client/app/list/item/item.jade",
"/Z/docker/data/lookup-lists/server/api/lookup/index.js",
"/Z/docker/data/lookup-lists/server/components/lookup/index.js",
"/Z/docker/data/lookup-lists/server/components/lookup/omegawiki.js",
"/Z/docker/data/lookup-lists/server/components/lookup/openlibrary.js",
"/Z/docker/data/lookup-lists/server/components/lookup/dbpedia.js",
"/Z/docker/data/lookup-lists/client/app/list/list.js",
"/Z/docker/data/lookup-lists/client/app/list/item/item.scss",
"/Z/docker/data/lookup-lists/server/components/lookup/panlex.js",
"/Z/docker/data/lookup-lists/client/app/list/item/item.controller.js",
"/C/Users/adam/AppData/Roaming/Sublime Text 3/Packages/User/Preferences.sublime-settings",
"/C/Users/adam/AppData/Roaming/Sublime Text 3/Packages/Default/Preferences.sublime-settings",
"/Z/docker/data/lookup-lists/client/app/list/index/index.jade",
"/Z/docker/data/lookup-lists/client/index.html",
"/Z/docker/data/lookup-lists/server/api/list/list.controller.js",
"/Z/docker/data/lookup-lists/server/api/lookup/controller.js",
"/Z/docker/data/lookup-lists/client/app/list/view/list.scss",
"/Z/docker/data/lookup-lists/client/app/list/edit/item.scss",
"/Z/docker/data/lookup-lists/server/config/express.js",
"/Z/docker/data/lookup-lists/client/app/list/edit/item.jade",
"/Z/docker/data/lookup-lists/client/app/list/edit/item.controller.js",
"/Z/docker/data/lookup-lists/server/config/environment/production.js",
"/Z/docker/data/lookup-lists/server/config/environment/test.js",
"/Z/docker/data/lookup-lists/server/config/environment/development.js",
"/Z/docker/data/lookup-lists/client/app/list/view/list.jade",
"/Z/docker/data/lookup-lists/server/api/list/index.js",
"/Z/docker/data/lookup-lists/client/app/list/view/list.controller.js",
"/Z/docker/docker-mean-dev/startup",
"/C/Windows/System32/drivers/etc/hosts",
"/Z/docker/data/lookup-lists/client/app/list/index/index.controller.js",
"/Z/docker/data/lookup-lists/client/app/list/edit/edit.controller.js",
"/Z/docker/data/lookup-lists/client/app/main/main.js",
"/Z/docker/data/lookup-lists/client/components/navbar/navbar.jade",
"/Z/docker/data/lookup-lists/client/app/app.scss",
"/Z/docker/data/lookup-lists/bower.json",
"/Z/docker/data/lookup-lists/client/app/lookup/lookup.jade",
"/Z/docker/data/lookup-lists/client/app/list/edit/edit.jade",
"/Z/docker/data/lookup-lists/client/app/list/index/username.filter.js",
"/Z/docker/data/lookup-lists/server/api/list/list.model.js",
"/Z/docker/data/lookup-lists/server/api/list/list.socket.js",
"/Z/docker/data/lookup-lists/client/components/navbar/navbar.controller.js",
"/Z/docker/data/lookup-lists/client/app/lists/lists.controller.js",
"/Z/docker/data/lookup-lists/client/app/lists/lists.jade",
"/Z/docker/data/lookup-lists/client/app/lists/lists.scss",
"/Z/docker/data/lookup-lists/client/app/lookup/lookup.js",
"/Z/docker/data/lookup-lists/server/config/local.env.js",
"/Z/docker/data/lookup-lists/server/config/seed.js",
"/Z/docker/data/lookup-lists/client/app/lists/lists.js",
"/Z/docker/data/lookup-lists/Gruntfile.js",
"/Z/docker/docker-mean-dev/Dockerfile",
"/Z/docker/docker-mean-dev/bootstrap.sh",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/api/lookup/index.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/index.html",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/app/lookup/lookup.controller.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/app/lookup/lookup.scss",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/app/lookup/lookup.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/package.json",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/routes.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/api/lookup/controller.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/app/lookup/lookup.jade",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/bower_components/angular-sanitize/angular-sanitize.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/Gruntfile.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/api/list/list.controller.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/components/lookup/omegawiki.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/components/lookup/index.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/bower_components/jquery/src/css/addGetHookIf.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/client/bower_components/jquery/src/wrap.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/api/lookup/omegawiki/index.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-lists/server/api/lookup/provider.js",
"/C/Users/adam/Google Drive/projects/contracts/2014 lieutenants pump/product/wireless network manifest.txt",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/views/sparql-query.jade",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/views/index.jade",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/config/seed.js",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/controllers/home.js",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/views/omegawiki.jade",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/controllers/sparql.js",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/controllers/omegawiki.js",
"/C/Users/adam/AppData/Local/Temp/Temp1_BabelNet-API-3.0.zip/BabelNet-API-3.0/licenses/README",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/app/views/sparql-results.jade",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/config/express.js",
"/C/Users/adam/Downloads/karl-foaf.xrdf",
"/C/Users/adam/Downloads/foaf.rdf",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/SPARQL examples.md",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/package.json",
"/C/Users/adam/Desktop/vagrant/node/learning-sparql/README.md",
"/C/Users/adam/Downloads/wibi-ver1.0/README.txt",
"/C/cygwin/home/adam/.profile",
"/C/cygwin/home/adam/.bash_profile",
"/C/cygwin/home/adam/.bashrc",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/cldr/wn-cldr-eng.tab",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/cldr/wn-cldr-afr.tab",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/wikt/wn-wikt-kur.tab",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/wikt/wn-wikt-ibl.tab",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/wikt/wn-wikt-hrx.tab",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/wikt/wn-wikt-dng.tab",
"/C/Users/adam/AppData/Local/Temp/Temp1_wn-wikt.zip/data/README",
"/C/Users/adam/Desktop/vagrant/node/language-learning-panlex/server/api/dictionary/language.model.js",
"/C/Users/adam/Desktop/vagrant/node/language-learning-panlex/server/api/dictionary/index.js",
"/C/Users/adam/Desktop/vagrant/node/language-learning-panlex/server/api/dictionary/dictionary.controller.js",
"/C/Users/adam/Desktop/vagrant/node/language-learning-panlex/.gitignore",
"/C/Users/adam/Desktop/vagrant/node/README.md",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-notes/.gitignore",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/api/note/note.model.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/routes.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/api/user/user.model.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/api/note/note.controller.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/config/seed.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/api/note/index.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/Gruntfile.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/package.json",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/config/local.env.js",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-hello/server/views/404.jade",
"/C/Users/adam/Desktop/vagrant/node/learning-mean-holmes/Gruntfile.js",
"/C/Users/adam/Desktop/vagrant/node/scripttest/Gruntfile.js",
"/C/Users/adam/Desktop/vagrant/node/scripttest/package.json"
],
"find":
{
"height": 47.0
},
"find_in_files":
{
"height": 117.0,
"where_history":
[
"C:\\Users\\adam\\Desktop\\vagrant\\node\\scripttest",
"C:\\Users\\adam\\Desktop\\git\\learning-nodejs-passport\\scripttest",
"<open folders>",
"C:\\Users\\adam\\Google Drive\\projects\\library-private\\website\\theme\\newlibrary-catalog-basic",
"C:\\Users\\adam\\Google Drive\\projects\\library-private\\website\\theme\\twentyfourteen",
"C:\\Users\\adam\\Google Drive\\projects\\library-public\\website\\theme\\twentyfourteen"
]
},
"find_state":
{
"case_sensitive": false,
"find_history":
[
"livereload",
"35729",
"!!!",
"open",
"runt.task.run",
"open",
"xdg",
"git",
" <",
"callback",
"ListViewCtrl",
" console.log(\"found one!\");\n console.log(resource);\n",
"isloggedin",
"user",
"!list.user == req.user",
"(!list.user == req.user)",
"open",
"xdg",
"fucks",
"main",
"open",
"wait",
"express",
"express:dev",
"aggregate",
"type",
"prop",
"promise",
"mongo",
"reload",
"signup",
"localhost",
"process.env.PORT",
"process.env",
"9000",
"serve:",
"local",
"127",
"local",
"TEMPLATE_DIRS",
"provision",
"[",
"python",
"flash",
"initializeCapture",
"elapsed",
"setupMouse",
"firebolt",
"h1",
"Owner",
"ruby",
"entry-footer",
"return_f",
" a ",
" a",
"h1",
"Alegreya Sans SC",
"entry-title",
"font-family",
"leto",
"script",
"font",
"newlibrary_catalog_basic_posted_on",
"date",
"newlibrary_catalog_basic_categorized_blog",
"thum",
"posts_to_fix",
"$html",
"external_image_options",
"add_media_page",
"menu",
"external_image_import_all_ajax",
"external_images_verify_permission",
"admin",
"json_encode",
"can",
"user_can",
"action",
"query",
"h3",
"h4",
"h5",
" ",
"</a>",
"<a",
"<a>",
"</a>",
"<ul>",
"<a name=\".*\"></a>",
" style=\"color: #ffffff;\"",
"blockquote",
":before",
"nth",
"home",
"2.0",
"10.1",
"get_sidebar",
"content-",
"content:",
"content",
"10.0",
"entry-title",
"10.0",
"2.0",
"add_query",
"wp_register",
"wp_register_file",
"wp_register_style",
"font",
"Lato",
"link",
"Lato",
"link",
"Lato",
"font",
"nth-child",
"nth-of",
"downlo",
"type",
".post-thumbnail",
".post-image",
"absolute",
"entry-header",
"FEATURED-CONTENT",
"FEATURED-CONTENT-INNER",
"archive-header",
"entry-header",
"#767676"
],
"highlight": true,
"in_selection": false,
"preserve_case": false,
"regex": false,
"replace_history":
[
"list.user != req.user",
"",
",",
"",
"\\n"
],
"reverse": false,
"show_context": true,
"use_buffer2": true,
"whole_word": false,
"wrap": true
},
"groups":
[
{
"selected": 5,
"sheets":
[
{
"buffer": 0,
"file": "client/index.html",
"semi_transient": false,
"settings":
{
"buffer_size": 3458,
"regions":
{
},
"selection":
[
[
0,
0
]
],
"settings":
{
"syntax": "Packages/HTML/HTML.tmLanguage",
"tab_size": 2,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 516.0,
"zoom_level": 1.0
},
"stack_index": 6,
"type": "text"
},
{
"buffer": 1,
"file": "bower.json",
"semi_transient": false,
"settings":
{
"buffer_size": 617,
"regions":
{
},
"selection":
[
[
0,
0
]
],
"settings":
{
"syntax": "Packages/JavaScript/JSON.tmLanguage",
"tab_size": 2,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 0.0,
"zoom_level": 1.0
},
"stack_index": 7,
"type": "text"
},
{
"buffer": 2,
"file": "client/app/app.js",
"semi_transient": false,
"settings":
{
"buffer_size": 349,
"regions":
{
},
"selection":
[
[
0,
0
]
],
"settings":
{
"syntax": "Packages/JavaScript/JavaScript.tmLanguage",
"tab_size": 2,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 0.0,
"zoom_level": 1.0
},
"stack_index": 5,
"type": "text"
},
{
"buffer": 3,
"file": "server/components/wdiff/index.js",
"semi_transient": false,
"settings":
{
"buffer_size": 8966,
"regions":
{
},
"selection":
[
[
590,
590
]
],
"settings":
{
"syntax": "Packages/Babel/JavaScript (Babel).sublime-syntax",
"tab_size": 2,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 168.0,
"zoom_level": 1.0
},
"stack_index": 4,
"type": "text"
},
{
"buffer": 4,
"file": "/C/Users/Adam/Desktop/code/courses/git-status-all",
"semi_transient": false,
"settings":
{
"buffer_size": 113,
"regions":
{
},
"selection":
[
[
113,
113
]
],
"settings":
{
"syntax": "Packages/Text/Plain text.tmLanguage"
},
"translation.x": 0.0,
"translation.y": 0.0,
"zoom_level": 1.0
},
"stack_index": 1,
"type": "text"
},
{
"buffer": 5,
"file": "/C/Users/Adam/Downloads/JavaScript Refactoring - Course Notes.md",
"semi_transient": false,
"settings":
{
"buffer_size": 2388,
"regions":
{
},
"selection":
[
[
0,
0
]
],
"settings":
{
"syntax": "Packages/Markdown/MultiMarkdown.sublime-syntax"
},
"translation.x": 0.0,
"translation.y": 0.0,
"zoom_level": 1.0
},
"stack_index": 0,
"type": "text"
},
{
"buffer": 6,
"file": "/C/Users/Adam/Dropbox (Envato)/Course Content/Code/ready/Create a New JavaScript Framework - How We Built the Daily Mail CMS/notes - QA.md",
"semi_transient": false,
"settings":
{
"buffer_size": 3928,
"regions":
{
},
"selection":
[
[
3928,
3928
]
],
"settings":
{
"syntax": "Packages/Markdown/MultiMarkdown.sublime-syntax",
"tab_size": 4,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 1786.0,
"zoom_level": 1.0
},
"stack_index": 2,
"type": "text"
},
{
"buffer": 7,
"file": "/C/Users/Adam/Dropbox (Envato)/Course Content/Code/ready/Create a New JavaScript Framework - Challenges of Rolling Your Own/notes - QA.md",
"semi_transient": false,
"settings":
{
"buffer_size": 4420,
"regions":
{
},
"selection":
[
[
1014,
1272
]
],
"settings":
{
"syntax": "Packages/Markdown/MultiMarkdown.sublime-syntax",
"tab_size": 4,
"translate_tabs_to_spaces": true
},
"translation.x": 0.0,
"translation.y": 379.0,
"zoom_level": 1.0
},
"stack_index": 3,
"type": "text"
}
]
}
],
"incremental_find":
{
"height": 26.0
},
"input":
{
"height": 35.0
},
"layout":
{
"cells":
[
[
0,
0,
1,
1
]
],
"cols":
[
0.0,
1.0
],
"rows":
[
0.0,
1.0
]
},
"menu_visible": true,
"output.exec":
{
"height": 207.0
},
"output.find_results":
{
"height": 0.0
},
"pinned_build_system": "",
"project": "markdown-format-wdiff.sublime-project",
"replace":
{
"height": 50.0
},
"save_all_on_build": true,
"select_file":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
[
"wait",
"client\\bower_components\\angular-sanitize\\angular-sanitize.js"
]
],
"width": 0.0
},
"select_project":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
],
"width": 0.0
},
"select_symbol":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
],
"width": 0.0
},
"selected_group": 0,
"settings":
{
},
"show_minimap": false,
"show_open_files": true,
"show_tabs": true,
"side_bar_visible": true,
"side_bar_width": 321.0,
"status_bar_visible": true,
"template_settings":
{
}
}

View File

@ -1,64 +1,92 @@
{
"name": "dubdiff",
"version": "2.0.1",
"description": "",
"main": "src/server/babel.index.js",
"scripts": {
"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: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",
"serve": "node src/server/babel.index.js",
"serve:prod": "cross-env NODE_ENV=production node src/server/babel.index.js",
"webpack-stats": "webpack --json > stats.json",
"lint": "standard --verbose | snazzy",
"lint:fix": "standard --fix --verbose | snazzy",
"test": "mocha --watch --compilers js:babel-register"
},
"author": "",
"license": "BSD-2-Clause",
"name": "markdown-format-wdiff",
"version": "0.0.0",
"main": "server/app.js",
"dependencies": {
"babel-preset-es2015-mod": "^6.6.0",
"babel-preset-es3": "^1.0.1",
"babel-preset-stage-2": "^6.18.0",
"body-parser": "^1.15.2",
"diff": "^3.0.1",
"express": "^4.14.0",
"isomorphic-fetch": "^2.2.1",
"jsonfile": "^2.4.0",
"markdown-it": "^5.1.0",
"markdown-to-jsx": "^4.0.3",
"react": "^0.14.5",
"react-dom": "^0.14.5",
"react-redux": "^4.4.6",
"react-router": "~3.0.0",
"redux": "^3.5.1",
"redux-thunk": "^2.1.0",
"request": "^2.79.0",
"request-promise-native": "^1.0.3",
"reselect": "^2.5.1",
"semantic-ui-css": "^2.2.4",
"semantic-ui-react": "^0.61.6",
"uuid": "^3.0.1"
"body-parser": "~1.5.0",
"chance": "~0.7.3",
"composable-middleware": "^0.3.0",
"compression": "~1.0.1",
"connect-mongo": "^0.4.1",
"cookie-parser": "~1.0.1",
"errorhandler": "~1.0.0",
"express": "~4.0.0",
"express-jwt": "^0.1.3",
"express-session": "~1.0.2",
"jade": "~1.2.0",
"jsonfile": "~2.0.0",
"jsonwebtoken": "^0.3.0",
"lex": "^1.7.8",
"lodash": "~2.4.1",
"method-override": "~1.0.0",
"mongoose": "~3.8.8",
"morgan": "~1.0.0",
"serve-favicon": "~2.0.1",
"temp": "^0.8.1"
},
"devDependencies": {
"babel-core": "^6.18.2",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-es2015-native-modules": "^6.9.4",
"babel-preset-node6": "^11.0.0",
"babel-preset-react": "^6.3.13",
"babel-register": "^6.18.0",
"chai": "^3.5.0",
"copyfiles": "^0.2.2",
"cpy-cli": "^1.0.1",
"cross-env": "^3.1.3",
"json-loader": "^0.5.4",
"mocha": "^3.2.0",
"piping": "^1.0.0-rc.4",
"snazzy": "^6.0.0",
"standard": "^8.6.0",
"webpack": "^2.1.0-beta.27"
}
"connect-livereload": "~0.4.0",
"grunt": "~0.4.4",
"grunt-angular-templates": "^0.5.4",
"grunt-asset-injector": "^0.1.0",
"grunt-autoprefixer": "~0.7.2",
"grunt-build-control": "DaftMonk/grunt-build-control",
"grunt-concurrent": "~0.5.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-concat": "~0.4.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-cssmin": "~0.9.0",
"grunt-contrib-htmlmin": "~0.2.0",
"grunt-contrib-imagemin": "~0.7.1",
"grunt-contrib-jade": "^0.11.0",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-uglify": "~0.4.0",
"grunt-contrib-watch": "~0.6.1",
"grunt-dom-munger": "^3.4.0",
"grunt-env": "~0.4.1",
"grunt-express-server": "~0.4.17",
"grunt-google-cdn": "~0.4.0",
"grunt-karma": "~0.8.2",
"grunt-mocha-test": "~0.10.2",
"grunt-newer": "~0.7.0",
"grunt-ng-annotate": "^0.2.3",
"grunt-node-inspector": "~0.1.5",
"grunt-nodemon": "~0.2.0",
"grunt-open": "~0.2.3",
"grunt-protractor-runner": "^1.1.0",
"grunt-rev": "~0.1.0",
"grunt-svgmin": "~0.4.0",
"grunt-usemin": "~2.1.1",
"grunt-wiredep": "~1.8.0",
"jit-grunt": "^0.5.0",
"jshint-stylish": "~0.1.5",
"karma": "~0.12.9",
"karma-chrome-launcher": "~0.1.3",
"karma-coffee-preprocessor": "~0.2.1",
"karma-firefox-launcher": "~0.1.3",
"karma-html2js-preprocessor": "~0.1.0",
"karma-jade-preprocessor": "0.0.11",
"karma-jasmine": "~0.1.5",
"karma-ng-html2js-preprocessor": "~0.1.0",
"karma-ng-jade2js-preprocessor": "^0.1.2",
"karma-ng-scenario": "~0.1.0",
"karma-phantomjs-launcher": "~0.1.4",
"karma-requirejs": "~0.2.1",
"karma-script-launcher": "~0.1.0",
"open": "~0.0.4",
"requirejs": "~2.1.11",
"should": "~3.3.1",
"supertest": "~0.11.0",
"time-grunt": "~0.3.1"
},
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"start": "node server/app.js",
"test": "grunt test",
"update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update"
},
"private": true
}

50
protractor.conf.js Normal file
View File

@ -0,0 +1,50 @@
// Protractor configuration
// https://github.com/angular/protractor/blob/master/referenceConf.js
'use strict';
exports.config = {
// The timeout for each script run on the browser. This should be longer
// than the maximum time your application needs to stabilize between tasks.
allScriptsTimeout: 110000,
// A base URL for your application under test. Calls to protractor.get()
// with relative paths will be prepended with this.
baseUrl: 'http://localhost:' + (process.env.PORT || '9000'),
// If true, only chromedriver will be started, not a standalone selenium.
// Tests for browsers other than chrome will not run.
chromeOnly: true,
// list of files / patterns to load in the browser
specs: [
'e2e/**/*.spec.js'
],
// Patterns to exclude.
exclude: [],
// ----- Capabilities to be passed to the webdriver instance ----
//
// For a full list of available capabilities, see
// https://code.google.com/p/selenium/wiki/DesiredCapabilities
// and
// https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js
capabilities: {
'browserName': 'chrome'
},
// ----- The test framework -----
//
// Jasmine and Cucumber are fully supported as a test and assertion framework.
// Mocha has limited beta support. You will need to include your own
// assertion framework if working with mocha.
framework: 'jasmine',
// ----- Options to be passed to minijasminenode -----
//
// See the full list at https://github.com/juliemr/minijasminenode
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};

15
server/.jshintrc Normal file
View File

@ -0,0 +1,15 @@
{
"node": true,
"esnext": true,
"bitwise": true,
"eqeqeq": true,
"immed": true,
"latedef": "nofunc",
"newcap": true,
"noarg": true,
"regexp": true,
"undef": true,
"smarttabs": true,
"asi": true,
"debug": true
}

11
server/.jshintrc-spec Normal file
View File

@ -0,0 +1,11 @@
{
"extends": ".jshintrc",
"globals": {
"describe": true,
"it": true,
"before": true,
"beforeEach": true,
"after": true,
"afterEach": true
}
}

View File

@ -0,0 +1,88 @@
'use strict';
var _ = require('lodash');
var Comparison = require('./comparison.model');
var wdiff = require('../../components/wdiff');
var jf = require('jsonfile');
var fs = require('fs');
//return the comparison given an id, if it exsits
exports.showComparison = function showComparison(req, res) {
//generate a filename
var filename = fnComparison(req.params.id);
//check if that file exists
fs.exists(filename, function (exists) {
//if the file does not exist, return a 404
if (!exists) return res.send(404);
//otherwise, read the file as JSON
jf.readFile(filename, function(err, comparison) {
if(err) { return handleError(res, err); }
//and return
return res.json(comparison);
});
});
}
//return a markdown wdiff for the comparison given an id, if it exsits
exports.wdiffMarkdownComparison = function wdiffMarkdownComparison(req, res) {
//generate a filename
var filename = fnComparison(req.params.id);
//check if that file exists
fs.exists(filename, function (exists) {
//if the file does not exist, return a 404
if (!exists) return res.send(404);
//otherwise, read the file as JSON
jf.readFile(filename, function(err, comparison) {
if(err) { return handleError(res, err); }
//now perform a wdiff on the result
wdiff(comparison.a,comparison.b, true, function(err, result) {
if (err)
return handleError(res, err);
_.merge(result, comparison)
return res.json(result);
});
});
});
}
// Creates a new comparison
exports.create = function create(req, res) {
var a = req.body.a;
var b = req.body.b;
//create the comparison
var comparison = new Comparison(a,b);
//look up its filename
var filename = fnComparison(comparison._id);
//and write it to the filesystem
jf.writeFile(filename, comparison, function(err) {
if(err) { return handleError(res, err); }
//if successful, return the comparison object
return res.json(201, comparison);
});
};
function handleError(res, err) {
console.log(err);
return res.send(500, err);
}
// returns a filename for the given comparison
function fnComparison (id) {
return "./data/" + "comp-" + id + ".json";
}

View File

@ -0,0 +1,19 @@
'use strict';
// Load Chance
var Chance = require('chance');
// Instantiate Chance so it can be used
var chance = new Chance();
var Comparison = function Comparison(a,b) {
return {
created: Date.now(),
a: a,
b: b,
_id: chance.hash()
};
};
module.exports = Comparison;

View File

@ -0,0 +1,18 @@
'use strict';
var express = require('express');
var controller = require('./comparison.controller');
var router = express.Router();
//router.get('/', controller.index);
router.get('/:id', controller.showComparison);
//router.get('/:id/:doc', controller.showComparisonDoc);
router.get('/wdiff/:id', controller.wdiffMarkdownComparison);
//router.get('/wdiff/:id/nomarkdown', controller.wdiffNoMarkdownComparison);
router.post('/', controller.create);
module.exports = router;

26
server/app.js Normal file
View File

@ -0,0 +1,26 @@
/**
* Main application file
*/
'use strict';
// Set default node environment to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var express = require('express');
var config = require('./config/environment');
// Setup server
var app = express();
var server = require('http').createServer(app);
require('./config/express')(app);
require('./routes')(app);
// Start server
server.listen(config.port, config.ip, function () {
console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
});
// Expose app
exports = module.exports = app;

View File

@ -0,0 +1,20 @@
/**
* Error responses
*/
'use strict';
module.exports[404] = function pageNotFound(req, res) {
var viewFilePath = '404';
var statusCode = 404;
var result = {
status: statusCode
};
res.status(result.status);
res.render(viewFilePath, function (err) {
if (err) { return res.json(result, result.status); }
res.render(viewFilePath);
});
};

View File

@ -0,0 +1,322 @@
'use strict';
var _ = require('lodash'),
temp = require('temp'),
fs = require('fs'),
exec = require('child_process').exec,
Lexer = require('lex');
var diff = require('diff');
// Automatically track and cleanup files at exit
temp.track();
// Perform a comparison between a and b
// the callback should have parameters (err, result)
module.exports = jsdiffEngine
function jsdiffEngine (a, b, asMarkdown, callback) {
//a few strings have to be escaped: "[-", "-]", "{+", and "+}"
a = escapeString(a)
b = escapeString(b)
var diffRes = diff.diffWordsWithSpace(a,b, {ignoreWhitespace:true})
var diffStr = diffRes.map (part => {
if (part.added) return "{+"+part.value+"+}";
else if (part.removed) return "[-"+part.value+"-]";
else return part.value;
}).join("");
//if no difference was found by wdiff, err.code will be 0
var wdiffSame;
wdiffSame = false; //???
console.log(diffStr)
var resData = {wdiffNoMarkdown:unescapeString(diffStr), same: wdiffSame};
if (asMarkdown) {
//!!! this needs more sophisticated parsing
var markdown = unescapeString(rewriteWdiffMarkdown(diffStr))
resData.wdiff=markdown;
}
return callback(null, resData);
}
function wdiffEngine (a, b, asMarkdown, callback) {
//!!! this nested file-open is not a good pattern
// better would be to use promises and write the two files asynchronously
//a few strings have to be escaped: "[-", "-]", "{+", and "+}"
a = escapeString(a)
b = escapeString(b)
// open the first file
temp.open('wdiffa-', function(err, filea) {
//handle errors
if (err)
return callback(err);
//write the string to the file
fs.write(filea.fd, a);
//close the file
fs.close(filea.fd, function(err) {
if (err)
return callback(err);
//open the second file
temp.open('wdiffa-', function(err, fileb) {
if (err)
return callback(err);
//write the string to the file
fs.write(fileb.fd, b);
//close the file
fs.close(fileb.fd, function(err) {
if (err)
return callback(err);
var cmd = "wdiff " + filea.path + " " +fileb.path;
exec(cmd, function(err, stdout) {
if (err && err.code!=1 && err.code!=0) {
return callback(err);
}
//if no difference was found by wdiff, err.code will be 0
var wdiffSame;
wdiffSame = (err && err.code == 0) ? true:false;
console.log(stdout)
var resData = {wdiffNoMarkdown:unescapeString(stdout), same: wdiffSame};
if (asMarkdown) {
//!!! this needs more sophisticated parsing
var markdown = unescapeString(rewriteWdiffMarkdown(stdout))
resData.wdiff=markdown;
}
return callback(null, resData);
});
});
});
});
});
}
/* Rewrites the given wdiff output to correctly render as markdown,
assuming the source documents were also valid markdown. */
function rewriteWdiffMarkdown(source) {
//initialize a stack for the lexed input
//make it a lodash container, just for kicks
var tokens = _([]);
//define tokens
var LDEL = {type:"LDEL"}, RDEL = {type:"RDEL"}, LINS = {type:"LINS"}, RINS = {type:"RINS"};
//var STRING = {type: "STRING", value:""};
var RDEL_LINS = {type:"RDEL_LINS"};
var NEWLINE = {type:"\n"};
var isStringToken = function (token) { return token.type == "STRING";}
//create a lexer to process the wdiff string
var lexer = new Lexer(function (char) {
//the default rule creates a string on the stack for unmatched characters
//and just adds characters to it as they come in
if (tokens.size() == 0 || !isStringToken(tokens.last()))
tokens.push({type: "STRING", value:""});
tokens.last().value += char;
});
//rules for the newline character,
//as well as opening and closing (left and right) delete and insert tokens
lexer
.addRule(/\[-/, function () {
tokens.push(LDEL);
})
.addRule(/-\]/, function () {
tokens.push(RDEL);
})
.addRule(/{\+/, function () {
tokens.push(LINS);
})
.addRule(/\+}/, function () {
tokens.push(RINS);
})
//we have a special rule for joined delete and insert tokens
.addRule(/-\] {\+/, function() {
tokens.push(RDEL_LINS);
})
.addRule(/\n/, function () {
//tokens.push({type:"STRING", value:"\n"})
tokens.push(NEWLINE);
})
;
//do the lexing
lexer.setInput(source);
lexer.lex();
//# now we parse and transform the input
//create a stack for the transformed output
var transform = _([]);
//set the state variables for the parse
var SSTRING = "string", SINS = "ins", SDEL = "del", SDELINS = "delins";
var state = SSTRING;
//this is the index of the immediately previous delete string in the transform stack
var deleteStartIndex = -1
//iterate the input tokens to create the intermediate representation
tokens.forEach(function(token) {
//we add string tokens to the transformed stack
if (isStringToken(token)) {
//add the string with state information
var item = {
string: token.value,
state: state
};
//if this is the DELINS state, we will put the string in the transformed stack in a different order
// the INS string is spliced into place just after the first DEL string
// the point of this is so that the preceeding markdown formatting instructions
// on this line are applied equally to the del and ins strings
// an extra space is inserted between DEL and INS items, for readibility
if (state == SDELINS) {
state = SINS;
item.state = SINS;
var spaceItem = {string: ' ', state: SSTRING};
transform.splice(deleteStartIndex+1, 0, item);
transform.splice(deleteStartIndex+1, 0, spaceItem);
}
else {
transform.push(item);
}
}
//the various tokens control the transformation mode
if (token == LDEL) {
state = SDEL;
deleteStartIndex = transform.size();
}
if (token == LINS) {
state = SINS;
}
if (token == RDEL || token == RINS) {
state = SSTRING;
deleteStartIndex = -1;
}
if (token == RDEL_LINS) {
state = SDELINS;
}
if (token == NEWLINE) {
transform.push({string: '\n', state: state});
}
//ignore newlines (they get added to the output)
});
// * now emit the output string
var output = "";
var newline = true;
var newlineIndex = -1;
// 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
//var PREFIX = /^([ \t]*\>)*(([ \t]*#*)|([ \t]*[\*\+-])|([ \t]*[\d]+\.))?[ \t]+/
var PREFIX = /^([ \t]*\>)*(([ \t]*#*)|([ \t]*[\*\+-])|([ \t]*[\d]+\.))?[ \t]*/
//var PREFIX = /^#*/
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;
}
});
return output;
}
function escapeString(str) {
str = str.replace(/\[\-/gm, "&#91;-")
str = str.replace(/\-\]/gm, "-&#93;")
return str
}
function unescapeString(str) {
str = str.replace(/\&\#91\;-/gm, "[-")
str = str.replace(/-\&\#93\;/gm, "-]")
return str
}

View File

@ -0,0 +1,12 @@
'use strict';
// Development specific configuration
// ==================================
module.exports = {
// MongoDB connection options
mongo: {
uri: 'mongodb://mongodb/markdownformatwdiff-dev'
},
seedDB: false
};

View File

@ -0,0 +1,50 @@
'use strict';
var path = require('path');
var _ = require('lodash');
function requiredProcessEnv(name) {
if(!process.env[name]) {
throw new Error('You must set the ' + name + ' environment variable');
}
return process.env[name];
}
// All configurations will extend these options
// ============================================
var all = {
env: process.env.NODE_ENV,
// Root path of server
root: path.normalize(__dirname + '/../../..'),
// Server port
port: process.env.PORT || 9000,
// Should we populate the DB with sample data?
seedDB: false,
// Secret for session, you will want to change this and make it an environment variable
secrets: {
session: 'markdown-format-wdiff-secret'
},
// List of user roles
userRoles: ['guest', 'user', 'admin'],
// MongoDB connection options
mongo: {
options: {
db: {
safe: true
}
}
},
};
// Export the config object based on the NODE_ENV
// ==============================================
module.exports = _.merge(
all,
require('./' + process.env.NODE_ENV + '.js') || {});

View File

@ -0,0 +1,23 @@
'use strict';
// Production specific configuration
// =================================
module.exports = {
// Server IP
ip: process.env.OPENSHIFT_NODEJS_IP ||
process.env.IP ||
undefined,
// Server port
port: process.env.OPENSHIFT_NODEJS_PORT ||
process.env.PORT ||
8080,
// MongoDB connection options
mongo: {
uri: process.env.MONGOLAB_URI ||
process.env.MONGOHQ_URL ||
process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME ||
'mongodb://mongodb/markdownformatwdiff'
}
};

View File

@ -0,0 +1,10 @@
'use strict';
// Test specific configuration
// ===========================
module.exports = {
// MongoDB connection options
mongo: {
uri: 'mongodb://localhost/markdownformatwdiff-test'
}
};

46
server/config/express.js Normal file
View File

@ -0,0 +1,46 @@
/**
* Express configuration
*/
'use strict';
var express = require('express');
var favicon = require('serve-favicon');
var morgan = require('morgan');
var compression = require('compression');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var cookieParser = require('cookie-parser');
var errorHandler = require('errorhandler');
var path = require('path');
var config = require('./environment');
var session = require('express-session');
module.exports = function(app) {
var env = app.get('env');
app.set('views', config.root + '/server/views');
app.set('view engine', 'jade');
app.use(compression());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(methodOverride());
app.use(cookieParser());
if ('production' === env) {
app.use(favicon(path.join(config.root, 'public', 'favicon.ico')));
app.use(express.static(path.join(config.root, 'public')));
app.set('appPath', config.root + '/public');
app.use(morgan('dev'));
}
if ('development' === env || 'test' === env) {
app.use(require('connect-livereload')());
app.use(express.static(path.join(config.root, '.tmp')));
app.use(express.static(path.join(config.root, 'client')));
app.set('appPath', 'client');
app.use(morgan('dev'));
app.use(errorHandler()); // Error handler - has to be last
}
};

View File

@ -0,0 +1,23 @@
'use strict';
// Use local.env.js for environment variables that grunt will set when the server starts locally.
// Use for your api keys, secrets, etc. This file should not be tracked by git.
//
// You will need to set these on the server you deploy to.
module.exports = {
DOMAIN: 'http://localhost:9000',
SESSION_SECRET: 'markdownformatwdiff-secret',
FACEBOOK_ID: 'app-id',
FACEBOOK_SECRET: 'secret',
TWITTER_ID: 'app-id',
TWITTER_SECRET: 'secret',
GOOGLE_ID: 'app-id',
GOOGLE_SECRET: 'secret',
// Control debug level for modules using visionmedia/debug
DEBUG: ''
};

24
server/routes.js Normal file
View File

@ -0,0 +1,24 @@
/**
* Main application routes
*/
'use strict';
var errors = require('./components/errors');
module.exports = function(app) {
// Insert routes below
app.use('/api/compare', require('./api/comparison'));
// All undefined asset or api routes should return a 404
app.route('/:url(api|components|app|bower_components|assets)/*')
.get(errors[404]);
// All other routes should redirect to the index.html
app.route('/*')
.get(function(req, res) {
res.sendfile(app.get('appPath') + '/index.html');
});
};

133
server/views/404.jade Normal file
View File

@ -0,0 +1,133 @@
doctype html
html(lang='en')
head
meta(charset='utf-8')
title Page Not Found :(
style.
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
html {
padding: 30px 10px;
font-size: 20px;
line-height: 1.4;
color: #737373;
background: #f0f0f0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
html,
input {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
body {
max-width: 500px;
_width: 500px;
padding: 30px 20px 50px;
border: 1px solid #b3b3b3;
border-radius: 4px;
margin: 0 auto;
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
background: #fcfcfc;
}
h1 {
margin: 0 10px;
font-size: 50px;
text-align: center;
}
h1 span {
color: #bbb;
}
h3 {
margin: 1.5em 0 0.5em;
}
p {
margin: 1em 0;
}
ul {
padding: 0 0 0 40px;
margin: 1em 0;
}
.container {
max-width: 380px;
_width: 380px;
margin: 0 auto;
}
/* google search */
#goog-fixurl ul {
list-style: none;
padding: 0;
margin: 0;
}
#goog-fixurl form {
margin: 0;
}
#goog-wm-qt,
#goog-wm-sb {
border: 1px solid #bbb;
font-size: 16px;
line-height: normal;
vertical-align: top;
color: #444;
border-radius: 2px;
}
#goog-wm-qt {
width: 220px;
height: 20px;
padding: 5px;
margin: 5px 10px 0 0;
box-shadow: inset 0 1px 1px #ccc;
}
#goog-wm-sb {
display: inline-block;
height: 32px;
padding: 0 10px;
margin: 5px 0 0;
white-space: nowrap;
cursor: pointer;
background-color: #f5f5f5;
background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
*overflow: visible;
*display: inline;
*zoom: 1;
}
#goog-wm-sb:hover,
#goog-wm-sb:focus {
border-color: #aaa;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
background-color: #f8f8f8;
}
#goog-wm-qt:hover,
#goog-wm-qt:focus {
border-color: #105cb6;
outline: 0;
color: #222;
}
input::-moz-focus-inner {
padding: 0;
border: 0;
}
body
.container
h1
| Not found
span :(
p Sorry, but the page you were trying to view does not exist.
p It looks like this was the result of either:
ul
li a mistyped address
li an out-of-date link
script.
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
script(src='//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js')

View File

@ -1,72 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import * as Actions from '../common/actions'
/* This component reads the local storage store and adds them to the Redux store.
* Local storage is read during the componentDidMount lifecycle method.
* Local storage is written during the componentWillReceiveProps lifecycle method.
*/
// an app-specific name for the localStorage state
const stateName = 'dubdiff_state'
// return a new object with the given keys, each assigned to the cooresponding value
// from the given object
const copyKeys = (obj, keys) => keys.reduce((acc, p) => { acc[p] = obj[p]; return acc }, {})
// utility method for retrieving json data from the local store
/*
function getLocalState (keys) {
if (window.localStorage.getItem(stateName)) {
const localState = JSON.parse(window.localStorage.getItem(stateName))
return copyKeys(localState, keys)
} else {
return copyKeys({}, keys)
}
}
*/
// utility method for writing json data to the local store
function setLocalState (state, keys) {
let toSave = copyKeys(state, keys)
window.localStorage.setItem(stateName, JSON.stringify(toSave))
}
const mapStateToProps = (state) => ({
input: state.input
// the loading/empty/clean state
})
const mapDispatchToProps = dispatch => ({
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text))
})
class LocalStorage extends React.Component {
// load the state from the local storage
componentDidMount () {
// only if the status is EMPTY
/*
if (this.props.input.original=='' && this.props.input.final == '') {
const localState = getLocalState(['input'])
if (localState.input && localState.input.original)
this.props.onChangeOriginal(localState.input.original)
if (localState.input && localState.input.final)
this.props.onChangeFinal(localState.input.final)
}
*/
}
// save the state to local storage
componentWillReceiveProps (nextProps) {
setLocalState(nextProps, ['input'])
}
render () {
return this.props.children
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LocalStorage)

View File

@ -1,88 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import * as Redux from 'redux'
import {Provider} from 'react-redux'
// import createBrowserHistory from 'history/lib/createBrowserHistory'
import {Router, browserHistory} from 'react-router'
import thunk from 'redux-thunk'
import * as reducers from '../common/reducers'
import routes from '../common/routes'
import {Format} from '../common/constants'
import * as Actions from '../common/actions'
import LocalStorage from './LocalStorage'
// initial state is rehydrated from the server
const initialState = JSON.parse(decodeURI(window.__INITIAL_STATE__))
// create the redux store
// initial state is retrieved from localStore
const store = Redux.createStore(
Redux.combineReducers(reducers),
initialState,
Redux.compose(
Redux.applyMiddleware(thunk),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
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 () {
ReactDOM.render(
<Provider store={store}>
<LocalStorage >
<Router history={browserHistory}>
{routes}
</Router>
</LocalStorage>
</Provider>
, document.getElementById('root'))
}

View File

@ -1,150 +0,0 @@
import fetch from 'isomorphic-fetch'
import uuid from 'uuid/v4'
import {browserHistory} from 'react-router'
import {Status, StatusError} from './constants'
// All state transitions in the app happen in these methods
// this includes redux state changes, asyncronous data requests, and browser location changes
export const updateOriginalInput = (text) =>
(dispatch, getState) => {
dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: text})
if (getState().input.original.length > 0) {
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
} else {
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
}
}
export const updateFinalInput = (text) =>
(dispatch, getState) => {
dispatch({type: 'UPDATE_FINAL_INPUT', data: text})
if (getState().input.final.length > 0) {
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
} else {
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
}
}
export const clearInput = () =>
(dispatch) => {
dispatch({type: 'CLEAR_INPUT'})
dispatch({type: 'STATUS_SET', data: Status.EMPTY})
}
export const setPlaintextFormat = () => ({ type: 'SET_PLAINTEXT_FORMAT' })
export const setMarkdownFormat = () => ({ type: 'SET_MARKDOWN_FORMAT' })
export const showOriginal = () => ({ type: 'SHOW_ORIGINAL' })
export const showFinal = () => ({ type: 'SHOW_FINAL' })
export const showDifference = () => ({ type: 'SHOW_DIFFERENCE' })
// if the input is dirty, saves it to the server
// creates a new uuid for the same,
// then changes the browser location to a comparison view with that id
export const compare = () =>
(dispatch, getState) => {
//! !! could test that the input is dirty before triggering a save
// if the input is empty, the compare should do nothing
// if the input is clean, the compare should not save and keep using the same id
// start saving the input to the server
const id = dispatch(save())
// we can use the id created by the save method to build a path
const comparePath = `/${id}`
browserHistory.replace(comparePath)
}
// clear the input and return to the edit page
export const reset = () =>
(dispatch, getState) => {
dispatch(clearInput())
browserHistory.push('/')
}
// switch to the edit view
export const edit = () =>
(dispatch, getState) => {
browserHistory.push('/')
}
// saves the current input fields to the server
// creates and returns a new id for the comparison
// should this method ensure that the initial state is valid? ('DIRTY')
export const save = () =>
(dispatch, getState) => {
// generate an id
const id = uuid()
// set waiting state
dispatch({type: 'STATUS_SET', data: Status.SAVING})
const endpointUri = `/api/compare/${id}`
const fetchOptions = {
method: 'POST',
body: JSON.stringify({
a: getState().input.original,
b: getState().input.final
}),
headers: {
'Content-Type': 'application/json'
}
}
// dispatch post request
fetch(endpointUri, fetchOptions)
.then(response => {
if (response.ok) {
dispatch({type: 'STATUS_SET', data: Status.CLEAN})
} else {
response.text().then((responseText) => {
const error = {message: `${response.status}: ${responseText}`}
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, error})
})
}
})
.catch(error => {
//! !! could use a better error message here
dispatch({type: 'STATUS_SET', data: Status.DIRTY})
dispatch({type: 'STATUS_SET_ERROR', data: StatusError.SAVE_ERROR, error})
})
// return the id after the request has been sent
return id
}
/*
const load = (id) =>
(dispatch, getState) => {
//set waiting state
dispatch( {type: 'SAVE_STATUS_WAITING'})
const endpointUri = `/api/compare/${id}`
const fetchOptions = {
method: 'GET'
}
//dispatch post request
fetch(endpointUri, fetchOptions)
.then(response => response.json())
.then(json => {
dispatch( {type: 'UPDATE_ORIGINAL_INPUT', data:json.a})
dispatch( {type: 'UPDATE_FINAL_INPUT', data:json.b})
dispatch( {type: 'LOAD_STATUS_LOADED'})
})
.catch(error => {
dispatch( {type: 'LOAD_STATUS_FAILED', error})
})
//return the id after the request has been sent
return id;
}
export const loadIfNeeded = (id) =>
(dispatch, getState) => {
if
}
*/

View File

@ -1,102 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import {Segment, Grid} from 'semantic-ui-react'
import * as Selectors from '../selectors'
import {Format} from '../constants'
import Header from './Header'
import Footer from './Footer'
import CompareControls from './CompareControls'
import ShowPlaintext from './ShowPlaintext'
import ShowMarkdown from './ShowMarkdown'
const mapStateToProps = (state) => ({
isMarkdownFormat: Selectors.isMarkdownFormat(state),
isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state),
safeInput: Selectors.safeInput(state),
diff: Selectors.diff(state)
})
const mapDispatchToProps = dispatch => ({
// loadIfNeeded: (id) => dispatch(Actions.loadIfNeeded())
})
class Compare extends React.Component {
/*
componentDidMount() {
this.props.loadIfNeeded(this.props.routeParams.compareId)
}
*/
render () {
return (
<div>
<Header />
<Segment basic padded>
<Grid stackable columns={2}>
<Grid.Column width='3'>
<CompareControls />
</Grid.Column>
<Grid.Column width='13'>
<Segment>
{
(!this.props.isMarkdownFormat && this.props.isShowDifference)
? <ShowPlaintext diff={this.props.diff}>{this.props.diff}</ShowPlaintext>
: (this.props.isMarkdownFormat && this.props.isShowDifference)
? <ShowMarkdown diff={this.props.diff}>{this.props.diff}</ShowMarkdown>
: (!this.props.isMarkdownFormat && !this.props.isShowDifference)
? <ShowPlaintext
text={this.props.isShowOriginal ? this.props.safeInput.original : this.props.safeInput.final}
/>
: (this.props.isMarkdownFormat && !this.props.isShowDifference)
? <ShowMarkdown
text={this.props.isShowOriginal ? this.props.safeInput.original : this.props.safeInput.final}
/>
: null
}
</Segment>
</Grid.Column>
</Grid>
</Segment>
<Footer />
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Compare)
/* <div ng-if="isMarkdownFormat">
<div ng-show="isShowBefore" class="col-md-10 col-sm-12 content-well">
<div btf-markdown="before" class="before">
</div>
</div>
<div ng-show="isShowWdiff" class="col-md-10 col-sm-12 content-well">
<div btf-markdown="wdiff" class="wdiff">
</div>
</div>
<div ng-show="isShowAfter" class="col-md-10 col-sm-12 content-well">
<div btf-markdown="after" class="after">
</div>
</div>
</div>
<div ng-if="!isMarkdownFormat">
<div ng-show="isShowBefore" class="col-md-10 col-sm-12 content-well">
<div ng-bind-html="before" class="content-pre before"></div>
</div>
<div ng-show="isShowWdiff" class="col-md-10 col-sm-12 content-well">
<div ng-bind-html="wdiff" class="content-pre wdiff"></div>
</div>
<div ng-show="isShowAfter" class="col-md-10 col-sm-12 content-well">
<div ng-bind-html="after" class="content-pre after"></div>
</div>
</div>
*/

View File

@ -1,60 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import {Button, Icon, Segment} from 'semantic-ui-react'
import * as Actions from '../actions'
import * as Selectors from '../selectors'
const mapStateToProps = (state) => ({
isMarkdownFormat: Selectors.isMarkdownFormat(state),
isShowOriginal: Selectors.isShowOriginal(state),
isShowFinal: Selectors.isShowFinal(state),
isShowDifference: Selectors.isShowDifference(state)
})
const mapDispatchToProps = dispatch => ({
onSetPlaintextFormat: () => dispatch(Actions.setPlaintextFormat()),
onSetMarkdownFormat: () => dispatch(Actions.setMarkdownFormat()),
onShowOriginal: () => dispatch(Actions.showOriginal()),
onShowFinal: () => dispatch(Actions.showFinal()),
onShowDifference: () => dispatch(Actions.showDifference()),
onEdit: () => dispatch(Actions.edit())
})
class CompareControls extends React.Component {
onClickMarkdownFormat () {
if (this.props.isMarkdownFormat) {
this.props.onSetPlaintextFormat()
} else {
this.props.onSetMarkdownFormat()
}
}
render () {
return (
<Segment.Group>
<Segment>
<Button fluid onClick={this.props.onEdit}>Edit</Button>
</Segment>
<Segment >
<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.onShowDifference} active={this.props.isShowDifference}>Difference</Button>
</Segment>
<Segment >
<Button fluid active={this.props.isMarkdownFormat} type='submit' onClick={this.onClickMarkdownFormat.bind(this)}>
{this.props.isMarkdownFormat ? <Icon name='checkmark' /> : <span />}
&nbsp;As Markdown
</Button>
</Segment>
</Segment.Group>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CompareControls)

View File

@ -1,11 +0,0 @@
import React from 'react'
import {Segment} from 'semantic-ui-react'
const Footer = (props) => (
<Segment basic padded textAlign='center' as='footer'>
<p><a href='http://adamarthurryan.com'>Adam Brown</a> | This website is <a href='https://github.com/adamarthurryan/dubdiff'>open source</a>.</p>
</Segment>
)
export default Footer

View File

@ -1,34 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import {Segment, Header, Rail} from 'semantic-ui-react'
import {Link} from 'react-router'
import * as Actions from '../actions'
import SaveStatus from './SaveStatus'
const mapStateToProps = (state) => ({
})
const mapDispatchToProps = dispatch => ({
onReset: () => { console.log(Actions.reset()); dispatch(Actions.reset()) }
})
const SiteHeader = (props) => (
<Segment basic >
<Segment basic padded textAlign='center' as='header' id='masthead'>
<Header><Link onClick={props.onReset}>dubdiff</Link></Header>
</Segment>
<Rail internal position='right'>
<Segment basic padded>
<SaveStatus />
</Segment>
</Rail>
</Segment>
)
export default connect(mapStateToProps, mapDispatchToProps)(SiteHeader)

View File

@ -1,63 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import {Segment, Grid, Form} from 'semantic-ui-react'
import * as Actions from '../actions'
import * as Selectors from '../selectors'
import Header from './Header'
import Footer from './Footer'
import MainControls from './MainControls'
const mapStateToProps = (state) => ({
input: state.input,
safeInput: Selectors.safeInput(state)
})
const mapDispatchToProps = dispatch => ({
onChangeOriginal: (text) => dispatch(Actions.updateOriginalInput(text)),
onChangeFinal: (text) => dispatch(Actions.updateFinalInput(text))
})
class Main extends React.Component {
render () {
return (
<div>
<Header />
<Segment basic padded>
<Grid stackable columns={3}>
<Grid.Column width='3'>
<MainControls />
</Grid.Column>
<Grid.Column width='6'>
<Form>
<Form.Field>
<label>Original</label>
<textarea value={this.props.input.original} onChange={event => this.props.onChangeOriginal(event.target.value)} />
</Form.Field>
</Form>
</Grid.Column>
<Grid.Column width='6'>
<Form>
<Form.Field>
<label>Final</label>
<textarea value={this.props.input.final} onChange={event => this.props.onChangeFinal(event.target.value)} />
</Form.Field>
</Form>
</Grid.Column>
</Grid>
</Segment>
<Footer />
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Main)

View File

@ -1,54 +0,0 @@
import React from 'react'
import {connect} from 'react-redux'
import {Button, Icon, Segment} from 'semantic-ui-react'
import * as Actions from '../actions'
import * as Selectors from '../selectors'
const mapStateToProps = (state) => ({
format: state.format,
isMarkdownFormat: Selectors.isMarkdownFormat(state),
saveStatus: state.saveStatus
})
const mapDispatchToProps = dispatch => ({
onSetPlaintextFormat: (format) => dispatch(Actions.setPlaintextFormat()),
onSetMarkdownFormat: (format) => dispatch(Actions.setMarkdownFormat()),
// returns an id for the record to be saved
onCompare: () => dispatch(Actions.compare())
})
class MainControls extends React.Component {
onClickMarkdownFormat () {
if (this.props.isMarkdownFormat) {
this.props.onSetPlaintextFormat()
} else {
this.props.onSetMarkdownFormat()
}
}
render () {
return (
<Segment.Group>
<Segment >
<Button fluid onClick={this.props.onCompare}>Compare</Button>
</Segment>
<Segment >
<Button fluid active={this.props.isMarkdownFormat} type='submit' onClick={this.onClickMarkdownFormat.bind(this)}>
{this.props.isMarkdownFormat ? <Icon name='checkmark' /> : <span />}
&nbsp;As Markdown
</Button>
</Segment>
</Segment.Group>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MainControls)
/*
<a type="button" onClick={this.onClickCompare.bind(this)} className="btn btn-block btn-primary">compare</a> */

View File

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

View File

@ -1,20 +0,0 @@
import React from 'react'
import markdownCompiler from 'markdown-to-jsx'
import {diffToHtml} from '../util/dubdiff'
const ShowMarkdown = (props) => {
return <div>
{
props.text
? markdownCompiler(props.text)
: props.diff
? markdownCompiler(diffToHtml(props.diff))
: null
}
</div>
}
export default ShowMarkdown

View File

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

View File

@ -1,24 +0,0 @@
export const Format = {
PLAINTEXT: 'PLAINTEXT',
MARKDOWN: 'MARKDOWN'
}
export const Show = {
ORIGINAL: 'ORIGINAL',
FINAL: 'FINAL',
DIFFERENCE: 'DIFFERENCE'
}
export const Status = {
INIT: 'INIT',
LOADING: 'LOADING',
EMPTY: 'EMPTY',
CLEAN: 'CLEAN',
DIRTY: 'DIRTY',
SAVING: 'SAVING'
}
export const StatusError = {
LOAD_ERROR: 'LOAD_ERROR',
SAVE_ERROR: 'SAVE_ERROR'
}

View File

@ -1,75 +0,0 @@
import {Format, Show, Status, StatusError} from './constants'
export function input (state, action) {
switch (action.type) {
case 'UPDATE_ORIGINAL_INPUT':
return Object.assign({}, state, {original: action.data})
case 'UPDATE_FINAL_INPUT':
return Object.assign({}, state, {final: action.data})
case 'CLEAR_INPUT':
return {original: '', final: ''}
default:
return state || {original: '', final: ''}
}
}
export function format (state, action) {
switch (action.type) {
case 'SET_PLAINTEXT_FORMAT':
return Format.PLAINTEXT
case 'SET_MARKDOWN_FORMAT':
return Format.MARKDOWN
default:
return state || Format.PLAINTEXT
}
}
export function show (state, action) {
switch (action.type) {
case 'SHOW_ORIGINAL':
return Show.ORIGINAL
case 'SHOW_FINAL':
return Show.FINAL
case 'SHOW_DIFFERENCE':
return Show.DIFFERENCE
default:
return state || Show.DIFFERENCE
}
}
/*
export function saveStatus (state, action) {
switch (action.type) {
case 'SAVE_STATUS_DIRTY':
return {dirty: true}
case 'SAVE_STATUS_EMPTY':
return {dirty: false, empty: true}
case 'SAVE_STATUS_SAVED':
return {dirty: false, saved: true}
case 'SAVE_STATUS_FAILED' :
return Object.assign({}, state, {waiting: false, failed: true, error: action.error})
case 'SAVE_STATUS_WAITING' :
return Object.assign({}, state, {waiting: true, failed: false, error: null})
default:
return state || {empty: true, dirty:false}
}
}
*/
// tracks status of the app, especially with respect to loaded and saved user data
export function status (state, action) {
// the status or error type is valid if it is in the list of Status or StatusError types
const isValidStatus = (type) => Status[type] === type
const isValidError = (type) => StatusError[type] === type
// the error is cleared when status changes
if (action.type === 'STATUS_SET' && isValidStatus(action.data)) {
return {type: action.data, error: null, hasError: false, errorType: null}
}
// the error is set in addition to the status
else if (action.type === 'STATUS_SET_ERROR' && isValidError(action.data)) {
return Object.assign({}, state, {error: action.error, hasError: true, errorType: action.data})
} else {
return state || {type: Status.EMPTY, hasError: false, error: null}
}
}

View File

@ -1,13 +0,0 @@
import {Route} from 'react-router'
import React from 'react'
import Main from './components/Main'
import Compare from './components/Compare'
var routes = [
<Route key='root' path='/' component={Main} />,
<Route key='compare' path='/:compareId' component={Compare} />
]
export default routes

View File

@ -1,59 +0,0 @@
// per http://redux.js.org/docs/recipes/ComputingDerivedData.html
import { createSelector } from 'reselect'
import React from 'react'
import {Format, Show} from './constants'
import * as Dubdiff from './util/dubdiff'
const input = (state) => state.input
const format = (state) => state.format
const show = (state) => state.show
export const safeInput = createSelector(
[input],
(input) => {
//! !! sanitize the input here and return
return input
}
)
export const isMarkdownFormat = createSelector(
[format],
(format) => {
return format === Format.MARKDOWN
}
)
const isShow = (type) => createSelector(
[show],
(show) => {
return show === type
}
)
export const isShowOriginal = isShow(Show.ORIGINAL)
export const isShowFinal = isShow(Show.FINAL)
export const isShowDifference = isShow(Show.DIFFERENCE)
export const diff = createSelector(
[format, safeInput],
(format, safeInput) => {
if (format === Format.PLAINTEXT) {
return Dubdiff.plaintextDiff(safeInput.original, safeInput.final)
} else if (format === Format.MARKDOWN) {
return Dubdiff.markdownDiff(safeInput.original, safeInput.final)
}
}
)
/*
html diff
---
diffHtml(parentOriginal, parentFinal) {
create stringOriginal, stringFinal consisting of
}
*/

View File

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

View File

@ -1,235 +0,0 @@
import EditorsDiff from './EditorsDiff'
let plaintextDiffer = new EditorsDiff()
let markdownDiffer = new EditorsDiff(/([\s,.:]|[*\[\]()])/)
// returns a comparison of the texts as plaintext
export function plaintextDiff (original, final) {
let diff = plaintextDiffer.diff(original, final)
return diff
}
// returns a comparison of the texts as markdown
export function markdownDiff (original, final) {
let diff = markdownDiffer.diff(original, final)
diff = rewriteMarkdownDiff(diff)
return diff
}
// returns a string version of the diff, with "{+ ... +}" and "[- ... -]"
// representing ins and del blocks
export function diffToString (diff, tags = {added: {start: '{+', end: '+}'}, removed: {start: '[-', end: '-]'}, same: {start: '', end: ''}}) {
return diff.map(({added, removed, value}) => {
let {start, end} = added ? tags.added : (removed ? tags.removed : tags.same)
let string = value
if (Array.isArray(value)) {
string = value.join('')
}
return start + string + end
}).join('')
}
export function diffToHtml (diff) {
return diffToString(diff, {added: {start: '<ins>', end: '</ins>'}, removed: {start: '<del>', end: '</del>'}, same: {start: '', end: ''}})
}
// 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. multiline ins and del blocks should be broken up into a series of single line blocks
// 3. after a newline, if an ins or del block begins with a markdown line formatting prefix (eg. for a title or list)
// then that prefix should be moved out of the block
// not yet implemented rules:
// 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 = applyTransformationRuleMultilineDelThenIns(transformedDiff)
transformedDiff = applyTransformationRuleBreakUpDelIns(transformedDiff)
transformedDiff = applyTransformationRuleFormattingPrefix(transformedDiff)
transformedDiff = applyTransformationRuleRemoveEmpty(transformedDiff)
return transformedDiff
}
// Transformation rule 1
// 1. if a multiline del block is followed by an ins block,
// the first line of the ins block should be inserted at the end of the first line of the del block
// so the markdown will apply to the ins text as it should
function applyTransformationRuleMultilineDelThenIns (diff) {
let transformedDiff = []
const B_ADDED = 'added'
const B_REMOVED = 'removed'
const B_SAME = 'same'
let previousBlockType = null
let currentBlockType = null
let previousBlockWasMultiline = false
let currentBlockIsMultiline = false
// iterate the input tokens to create the intermediate representation
diff.forEach((currentBlock) => {
previousBlockType = currentBlockType
previousBlockWasMultiline = currentBlockIsMultiline
currentBlockType = (currentBlock.added ? B_ADDED : (currentBlock.removed ? B_REMOVED : B_SAME))
currentBlockIsMultiline = isMultilineDiffBlock(currentBlock)
// transform rule 1 applys when:
// the previous block was a del and had multiple lines
// the current block is an ins
if (previousBlockType === B_REMOVED && currentBlockType === B_ADDED && previousBlockWasMultiline) {
// split the first line from the current block
let currentBlockSplit = splitMultilineDiffBlock(currentBlock)
// pop the previous diff entry
let previousBlock = transformedDiff.pop()
// split the first line from the previous block
let previousBlockSplit = splitMultilineDiffBlock(previousBlock)
// now add the blocks back, interleaving del and ins blocks
for (let i = 0; i < Math.max(previousBlockSplit.length, currentBlockSplit.length); i++) {
if (i < previousBlockSplit.length) {
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
}
// 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
// ^ - start of line
// ([ \t]*\>)* - blockquotes (possibly nested)
// (
// ([ \t]*#*) - headers
// |([ \t]+[\*\+-]) - unordered lists
// |([ \t]+[0-9]+\.) - numeric lists
// )?
// [ \t]+ - trailing whitespace
const MARKDOWN_PREFIX = /^([ \t]*>)*(([ \t]*#*)|([ \t]*[*+\-])|([ \t]*[\d]+\.))?[ \t]+/
// matches strings that end with a newline followed by some whitespace
const NEWLINE_SUFFIX = /\n\s*$/
// 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)
// then that prefix should be moved out of the block
// also, if an ins block begins with a formatting prefix and follows immediately after a del block that follows a newline,
// the prefix should be moved out of the block _and_ an extra newline character should be added to the beginning of it
function applyTransformationRuleFormattingPrefix (diff) {
let transformedDiff = []
let isNewline = true
let newlineString = '\n'
// iterate the input tokens to create the intermediate representation
diff.forEach((currentBlock) => {
if (isNewline && (currentBlock.added || currentBlock.removed)) {
let match = currentBlock.value.match(MARKDOWN_PREFIX)
if (match) {
let preBlock = {value: match[0]}
let postBlock = {added: currentBlock.added, removed: currentBlock.removed, value: currentBlock.value.substring(match[0].length)}
if (currentBlock.added) {
let newlineBlock = {value: newlineString}
transformedDiff.push(newlineBlock)
}
transformedDiff.push(preBlock)
transformedDiff.push(postBlock)
} else {
transformedDiff.push(currentBlock)
}
} else {
transformedDiff.push(currentBlock)
isNewline = NEWLINE_SUFFIX.test(currentBlock.value)
if (isNewline) {
newlineString = currentBlock.value.match(NEWLINE_SUFFIX)[0]
}
}
})
return transformedDiff
}
// returns true if the given diff block contains a newline element
function isMultilineDiffBlock ({value}) {
return value.indexOf('\n') !== -1
}
// returns an array of diff blocks that have the same added, removed fields as the given one
// but with the string split by newlines
// if the diff block has no newlines, an array containing only that diff will be returned
// if the diff block has newlines, the resulting array will have a series of blocks,
// consisting of the block text, interleaved with newlines
// ,
// each of which begin with a newline
// if the diff block begins with a newline, the returned array will begin with an empty diff
function splitMultilineDiffBlock ({added, removed, value}) {
let lines = value.split('\n')
let blocks = []
// lines = lines.filter(line=>line.length>0)
lines.forEach((line, index) => {
blocks.push({added, removed, value: line})
if (index < lines.length - 1) blocks.push({value: '\n'})
})
return blocks
}

View File

@ -1,37 +0,0 @@
var Path = require('path')
var srcRoot = Path.join(__dirname, '..')
// there should be some option for distribution / optimization?
var config = {
presets: ['node6', 'react'],
// enable source maps for non-production instances
sourceMaps: (process.env.NODE_ENV !== 'production' ? 'both' : false),
// highlightCode: false,
sourceRoot: srcRoot,
only: /src/
}
require('babel-core/register')(config)
var piping = require('piping')
main()
function main () {
// Enable piping for non-production environments
if (process.env.NODE_ENV !== 'production') {
// piping will return false for the initial invocation
// the app will be run again in an instance managed by piping
if (!piping({hook: true, includeModules: false})) {
return
}
}
try {
require('./index.js')
} catch (error) {
console.error(error.stack)
}
}

View File

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

View File

@ -1,83 +0,0 @@
import express from 'express'
import path from 'path'
import bodyParser from 'body-parser'
import * as Redux from 'redux'
import fetch from 'isomorphic-fetch'
import comparisonRouter from './comparison'
import * as reducers from '../common/reducers'
import {Status, StatusError} from '../common/constants'
import render from './render'
// set use port 8080 for dev, 80 for production
const PORT = (process.env.NODE_ENV !== 'production' ? 8080 : 80)
const app = express()
// serve the dist static files at /dist
app.use('/dist', express.static(path.join(__dirname, '..', '..', 'dist')))
// serve the comparison api at /api/compare
app.use(bodyParser.json())
app.use('/api/compare', comparisonRouter)
// the following routes are for server-side rendering of the app
// we should render the comparison directly from the server
// this loading logic could be moved into ../common/actions because it is isomorphic
app.route('/:comparisonId')
.get((req, res) => {
const store = createSessionStore()
const endpointUri = `http://localhost:${PORT}/api/compare/${req.params.comparisonId}`
// fetch the comparison
fetch(endpointUri)
.then(response => {
if (response.ok) {
return response.json()
} else {
response.text().then(() => {
const error = {message: `${response.status}: ${response.statusText}`}
initAndRenderError(error, store, req, res)
})
}
})
.then(({a, b}) => {
initAndRenderComparison({a, b}, store, req, res)
})
.catch(error => {
initAndRenderError(error, store, req, res)
})
})
app.route('/')
.get((req, res) => {
render(createSessionStore(), req, res)
})
app.listen(PORT, function () {
console.log(`Server listening on port ${PORT}.`)
})
// creates the session store
function createSessionStore () {
// create the redux store
return Redux.createStore(
Redux.combineReducers(reducers)
)
}
function initAndRenderComparison ({a, b}, store, req, res) {
store.dispatch({type: 'UPDATE_ORIGINAL_INPUT', data: a})
store.dispatch({type: 'UPDATE_FINAL_INPUT', data: b})
store.dispatch({type: 'STATUS_SET', data: Status.CLEAN})
render(store, req, res)
}
function initAndRenderError (error, store, req, res) {
store.dispatch({type: 'STATUS_SET', data: Status.EMPTY})
store.dispatch({type: 'STATUS_SET_ERROR', data: StatusError.LOAD_ERROR, error})
render(store, req, res)
}

View File

@ -1,83 +0,0 @@
import React from 'react'
import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux'
import { match, RouterContext } from 'react-router'
import routes from '../common/routes.js'
export default function render (store, req, res) {
// Send the rendered page back to the client
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(errorTemplate('Routing Error:', error.message))
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// Render the component to a string
try {
const html = renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
)
// Grab the initial state from our Redux store
const initialState = store.getState()
// and send
res.status(200).send(appTemplate(html, initialState))
} catch (ex) {
console.log('Render Exception:', ex)
res.status(500).send(errorTemplate('Render Exception', ex.message, ex))
}
} else {
res.status(404).send(errorTemplate('Not found', `${req.url} not found.`))
}
})
}
const pageTemplate = (body) => {
return `
<!doctype html>
<html>
<head>
<title>Dubdiff</title>
<!-- CSS -->
<link rel="stylesheet" href="dist/semantic.min.css"/>
<link rel="stylesheet" href="dist/main.css"/>
<!-- Favicon -->
<link rel="icon" type="image/png" sizes="32x32" href="dist/favicon-32x32.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">
</head>
<body>
${body}
</body>
</html>
`
}
function errorTemplate (title, message, exception) {
return pageTemplate(`
<h1>${title}</h1>
<p>${message}</p>
${exception
? `<pre>${exception.toString()}</pre>`
: ``
}
`)
}
function appTemplate (html, initialState) {
return pageTemplate(`
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = "${encodeURI(JSON.stringify(initialState, null, 2))}"
</script>
<!-- <script>__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__</script> -->
<script type="text/javascript" src="dist/browser-bundle.js"></script>
`)
}

1
test/a.txt Normal file
View File

@ -0,0 +1 @@
lorem ipsum

1
test/b.txt Normal file
View File

@ -0,0 +1 @@
lorum ipsum

View File

@ -1,85 +0,0 @@
/* eslint-env node, mocha */
/* global expect */
/* eslint no-console: 0 */
'use strict'
import chai from 'chai'
import {markdownDiff, diffToString} from '../src/common/util/dubdiff'
let diff = (a, b) => diffToString(markdownDiff(a, b))
const expect = chai.expect // eslint-disable-line no-unused-vars
describe('dubdiff', () => {
beforeEach(() => {
})
it('plaintext diffs consecutive words', () => {
expect(diff(
'This is a smlb sentnce with no errors.',
'This is a simple sentence with no errors.'
)).to.equal('This is a [-smlb sentnce -]{+simple sentence +}with no errors.')
})
it('plaintext diffs with word deletion', () => {
expect(diff(
'Gonna delete a word.',
'Gonna delete word.'
)).to.equal('Gonna delete [-a -]word.')
})
it('plaintext diffs with word insertion', () => {
expect(diff(
'Gonna delete word.',
'Gonna delete a word.'
)).to.equal('Gonna delete {+a +}word.')
})
it('reorganizes insertions after multiline deletions', () => {
expect(diff(
`# Title
other`,
`# Subtitle`
)).to.equal('# [-Title-]{+Subtitle+}\n[-other-]')
})
it('pulls prefixes out of ins or del blocks after newline', () => {
expect(diff(
'# Title\n > hello',
'# Title\n - goodbye'
)).to.equal('# Title\n > [-hello-]\n - {+goodbye+}')
})
it('respects bold and italic boundaries', () => {
expect(diff(
'This *word* **isn\'t** changed.',
'This *other one* **is** changed.'
)).to.equal('This *[-word-]{+other one+}* **[-isn\'t-]{+is+}** changed.')
})
it('respects link boundaries in link text', () => {
expect(diff(
'This [link](https://somewhere.com) is the same.',
'This [target](https://somewhere.com) changed.'
)).to.equal('This [[-link-]{+target+}](https://somewhere.com) [-is the same-]{+changed+}.')
})
it('respects link boundaries in link target', () => {
expect(diff(
'This [link](https://somewhere.com) is the same.',
'This [link](https://somewhere.org) changed.'
)).to.equal('This [link](https://somewhere.[-com-]{+org+}) [-is the same-]{+changed+}.')
})
it('deletes a title', () => {
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,68 +0,0 @@
/* eslint-env node, mocha */
/* global expect */
/* eslint no-console: 0 */
'use strict'
import chai from 'chai'
import {plaintextDiff, diffToString} from '../src/common/util/dubdiff'
let diff = (a, b) => diffToString(plaintextDiff(a, b))
const expect = chai.expect // eslint-disable-line no-unused-vars
describe('dubdiff', () => {
beforeEach(() => {
})
it('diffs single words', () => {
expect(diff(
'This is a smlb sentence.',
'This is a simple sentence.'
)).to.equal('This is a [-smlb -]{+simple +}sentence.')
})
it('diffs consecutive words', () => {
expect(diff(
'This is a smlb sentnce with no errors.',
'This is a simple sentence with no errors.'
)).to.equal('This is a [-smlb sentnce -]{+simple sentence +}with no errors.')
})
it('diffs with word deletion', () => {
expect(diff(
'Gonna delete a word.',
'Gonna delete word.'
)).to.equal('Gonna delete [-a -]word.')
})
it('diffs with word insertion', () => {
expect(diff(
'Gonna add word.',
'Gonna add a word.'
)).to.equal('Gonna add {+a +}word.')
})
it('diffs accross newline without weird spaces', () => {
expect(diff(
'This is a flawed\ncomment',
'This is a corrected\nitem'
)).to.equal('This is a [-flawed-]{+corrected+}\n[-comment-]{+item+}')
})
it('doesn\'t add spaces after newline', () => {
expect(diff(
'\nhere',
'\nhere'
)).to.equal('\nhere')
})
it('doesn\'t add spaces before newline', () => {
expect(diff(
'there\n',
'there\n'
)).to.equal('there\n')
})
it('treats punctuation separately', () => {
expect(diff(
'Hello world.',
'Hello, world.'
)).to.equal('Hello{+, +}world.')
})
})

View File

@ -1,35 +0,0 @@
let config = {
cache: true,
entry: './src/client/index.js',
output: {
filename: './dist/browser-bundle.js'
},
target: 'web',
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2015-native-modules', 'react'],
compact: 'true'
}
},
{ test: /\.json$/, loader: 'json-loader' }
]
},
node: {
fs: 'empty',
net: 'empty',
tls: 'empty'
}
}
if (process.env.NODE_ENV === 'production') {
config.devtool = 'cheap-module-source-map'
} else {
config.devtool = 'eval'
}
module.exports = config