diff --git a/client/app/app.scss b/client/app/app.scss
index 140d478..aa9f0bf 100644
--- a/client/app/app.scss
+++ b/client/app/app.scss
@@ -15,10 +15,21 @@ $fa-font-path: "/bower_components/font-awesome/fonts";
padding: 0.2em 0;
}
+table.revisions th, table.revisions td{
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+table.revisions .state {
+ width: 7em;
+}
+
// Component styles are injected through grunt
// injector
@import 'account/login/login.scss';
@import 'admin/admin.scss';
+@import 'document/document.scss';
+@import 'document/revision-new/revision-new.scss';
@import 'wdiff/wdiff.scss';
@import 'modal/modal.scss';
// endinjector
\ No newline at end of file
diff --git a/client/app/document/document.scss b/client/app/document/document.scss
new file mode 100644
index 0000000..871c6a1
--- /dev/null
+++ b/client/app/document/document.scss
@@ -0,0 +1,3 @@
+table.revisions {
+ column-gap: 2em;
+}
\ No newline at end of file
diff --git a/client/app/document/index/index.controller.js b/client/app/document/index/index.controller.js
index a1a012a..0b0cbef 100644
--- a/client/app/document/index/index.controller.js
+++ b/client/app/document/index/index.controller.js
@@ -2,8 +2,10 @@
angular.module('markdownFormatWdiffApp')
.controller('DocumentIndexCtrl', function ($scope, $routeParams, $http, Auth, $location) {
+ $scope.title = 'Documents';
+
$scope.documents = [];
- $scope.newDocumentTitle;
+ $scope.newDocumentTitle = '';
$scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn;
diff --git a/client/app/document/index/index.jade b/client/app/document/index/index.jade
index d2d881f..9f9784b 100644
--- a/client/app/document/index/index.jade
+++ b/client/app/document/index/index.jade
@@ -1,10 +1,10 @@
nav(ng-include='"components/navbar/navbar.html"')
-nav(ng-include='"components/elements/header.html"', onload='title = "documents"')
+nav(ng-include='"components/elements/header.html"')
.container
.row(ng-show='isLoggedIn()')
- .col-lg-9.col-md-12.center-block
+ .col-lg-9.col-md-12
form.form-inline
.form-group
label(for='title-input')
@@ -15,17 +15,27 @@ nav(ng-include='"components/elements/header.html"', onload='title = "documents"'
.row
.col-lg-6.col-md-12(ng-repeat='document in documents | orderBy:"currentRevision.created":true' )
- h4
+ h1
a(href='/{{document._id}}')
{{document.title}}
- div
+ table.revisions
+ tr
+ th.state State
+ th.created revision
+ tr(ng-repeat='revision in document.revisions | orderBy:"created":true | limitTo:5' )
+ td.state {{revision.state}}
+ td.created
+ h4
+ a(href='/{{document._id}}/revision/{{revision._id}}')
+ {{revision.created}}
+ //div
p
| State:
{{document.currentRevision.state}}
p
| Updated:
{{document.currentRevision.created}}
- pre
+ //pre
{{json(document)}}
diff --git a/client/app/document/revision-new/revision-new.controller.js b/client/app/document/revision-new/revision-new.controller.js
index 9beeaa3..b45e90d 100644
--- a/client/app/document/revision-new/revision-new.controller.js
+++ b/client/app/document/revision-new/revision-new.controller.js
@@ -2,8 +2,12 @@
angular.module('markdownFormatWdiffApp')
.controller('DocumentRevisionNewCtrl', function ($scope, $routeParams, $http, Auth, $location) {
+ $scope.title = '';
+ $scope.subtitle = '';
+
$scope.revision = {};
+ $scope.stateOptions = ['first draft', 'final draft', 'first edit', 'final edit'];
$scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn;
@@ -17,13 +21,16 @@ angular.module('markdownFormatWdiffApp')
};
var path = '/api/documents/' + $routeParams.id;
- $http.get(path).success(function(revision) {
+ $http.get(path).success(function(document) {
$scope.document = document;
$scope.revision = angular.copy(document.currentRevision);
+ $scope.title = document.title;
+ $scope.subtitle = 'new revision';
});
$scope.saveRevision = function() {
+ alert(JSON.stringify($scope.revision))
//save the revision to the document
$http.post('/api/documents/'+$routeParams.id+'/revisions', $scope.revision)
.success(function(newRevision) {
diff --git a/client/app/document/revision-new/revision-new.jade b/client/app/document/revision-new/revision-new.jade
index 470b329..e327622 100644
--- a/client/app/document/revision-new/revision-new.jade
+++ b/client/app/document/revision-new/revision-new.jade
@@ -1,24 +1,30 @@
nav(ng-include='"components/navbar/navbar.html"')
-nav(ng-include='"components/elements/header.html"', ng-onload='title = {{revision.created}}')
+nav(ng-include='"components/elements/header.html"')
.container
.row
- form.col-lg-9.col-md-12.center-block
- h4
- {{revision.document.title}} - {{revision.created}}
- .form-group
- label(for='status-input')
- | Status
- input.form-control(type='text', id='status-input', ng-model='revision.status')
- .form-group
- label(for='content-input')
- | Content
- textarea.form-control(id='content-input', ng-model='revision.content')
- button.btn.btn-default(ng-click='saveRevision()')
- | Save
+ .col-lg-6.col-md-12
+ form
+ h4
+ {{document.title}} - {{revision.created}}
+ .form-group
+ label(for='status-input')
+ | Status
+ select.form-control(id='status-input', ng-model='revision.state', ng-options='stateOption as stateOption for stateOption in stateOptions')
+ option(value='{{stateOption}}')
+ //input.form-control(type='text', id='status-input', ng-model='revision.state')
+ .form-group
+ label(for='content-input')
+ | Content
+ textarea.form-control(id='content-input', ng-model='revision.content')
+ button.btn.btn-primary(ng-click='saveRevision()')
+ | Save
- .row
+ .col-lg-6.col-md-12
+ div(btf-markdown='revision.content')
+
+ //.row
pre.col-lg-9.col-md-12.center-block
{{json(revision)}}
diff --git a/client/app/document/revision-new/revision-new.scss b/client/app/document/revision-new/revision-new.scss
new file mode 100644
index 0000000..aef7853
--- /dev/null
+++ b/client/app/document/revision-new/revision-new.scss
@@ -0,0 +1,20 @@
+#content-input {
+ resize: vertical;
+ min-height: 200px;
+}
+
+
+@media (min-width: $screen-md-min) {
+ #content-input {
+ resize: vertical;
+ min-height: 300px;
+ }
+}
+
+@media (min-width: $screen-lg-min) {
+ #content-input {
+ resize: vertical;
+ min-height: 450px;
+ }
+}
+
diff --git a/client/app/document/revision/revision.controller.js b/client/app/document/revision/revision.controller.js
index 38f00e9..7f6855d 100644
--- a/client/app/document/revision/revision.controller.js
+++ b/client/app/document/revision/revision.controller.js
@@ -4,6 +4,9 @@ angular.module('markdownFormatWdiffApp')
.controller('DocumentRevisionCtrl', function ($scope, $routeParams, $http, Auth) {
$scope.revision = {};
+ $scope.title = '';
+ $scope.subtitle = '';
+
$scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn;
@@ -27,6 +30,8 @@ angular.module('markdownFormatWdiffApp')
var path = '/api/documents/'+$routeParams.id+'/revisions/' + $routeParams.revisionid;
$http.get(path).success(function(revision) {
$scope.revision = revision;
+ $scope.title = revision.document.title;
+ $scope.subtitle = $scope.isCurrent() ? " (current)":(" ("+$scope.revision.created+")");
});
$scope.json = function (object) { return JSON.stringify(object, null, " "); };
diff --git a/client/app/document/revision/revision.jade b/client/app/document/revision/revision.jade
index fa8dbd2..d8b119c 100644
--- a/client/app/document/revision/revision.jade
+++ b/client/app/document/revision/revision.jade
@@ -1,27 +1,22 @@
nav(ng-include='"components/navbar/navbar.html"')
-nav(ng-include='"components/elements/header.html"', ng-onload='title = {{revision.created}}')
+nav(ng-include='"components/elements/header.html"')
.container
.row
- span(ng-show='isCurrent()')
+ //span(ng-show='isCurrent()')
| Current revision
a.btn.btn-primary(ng-hide='isCurrent()' href='/wdiff/{{revision._id}}/{{revision.document.currentRevision}}')
| wdiff current
.row
- .col-lg-6.col-md-9.center-block
- h4
- {{revision.created}}
+ .col-lg-6.col-md-9
div
p
| State:
- {{revision.state}}
- p
- | Updated:
- {{revision.created}}
+ {{revision.state}}
div(btf-markdown='revision.content')
- pre
+ //pre
{{json(revision)}}
diff --git a/client/app/document/show/show.controller.js b/client/app/document/show/show.controller.js
index f4d9439..bc053c1 100644
--- a/client/app/document/show/show.controller.js
+++ b/client/app/document/show/show.controller.js
@@ -3,6 +3,7 @@
angular.module('markdownFormatWdiffApp')
.controller('DocumentShowCtrl', function ($scope, $routeParams, $http, Auth) {
$scope.document = {};
+ $scope.title = '';
$scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn;
@@ -19,6 +20,7 @@ angular.module('markdownFormatWdiffApp')
var path = '/api/documents/' + $routeParams.id;
$http.get(path).success(function(document) {
$scope.document = document;
+ $scope.title = document.title;
});
$scope.json = function (object) { return JSON.stringify(object, null, " "); };
diff --git a/client/app/document/show/show.jade b/client/app/document/show/show.jade
index f620479..56e5683 100644
--- a/client/app/document/show/show.jade
+++ b/client/app/document/show/show.jade
@@ -1,26 +1,28 @@
nav(ng-include='"components/navbar/navbar.html"')
-nav(ng-include='"components/elements/header.html"', ng-onload='title = {{document.title}}')
+nav(ng-include='"components/elements/header.html"')
.container
.row(ng-show='isOwner()')
- form.col-lg-12
- a.btn.btn-primary(href='/{{document._id}}/revision/new')
- | New Revision
+ .col-lg-12
+ form
+ a.btn.btn-primary(href='/{{document._id}}/revision/new')
+ | New Revision
- .row(ng-repeat='revision in document.revisions | orderBy:"created":true' )
+ .row
.col-lg-6.col-md-9.center-block
- h4
- a(href='/{{document._id}}/revision/{{revision._id}}')
- {{revision.created}}
- div
- p
- | State:
- {{revision.state}}
- p
- | Updated:
- {{revision.created}}
- pre
+
+ table.revisions
+ tr
+ th.state State
+ th.created revision
+ tr(ng-repeat='revision in document.revisions | orderBy:"created":true | limitTo:5' )
+ td.state {{revision.state}}
+ td.created
+ h4
+ a(href='/{{document._id}}/revision/{{revision._id}}')
+ {{revision.created}}
+ //pre
| {{json(revision)}}
diff --git a/client/app/document/wdiff/wdiff.controller.js b/client/app/document/wdiff/wdiff.controller.js
index 657586c..979090c 100644
--- a/client/app/document/wdiff/wdiff.controller.js
+++ b/client/app/document/wdiff/wdiff.controller.js
@@ -2,7 +2,8 @@
angular.module('markdownFormatWdiffApp')
.controller('DocumentWdiffCtrl', function ($scope, $routeParams, $http, Auth) {
- $scope.wdiff = "";
+ $scope.wdiff = '';
+ $scope.title = '';
$scope.same = false;
$scope.revisionA = {};
$scope.revisionB = {};
@@ -33,6 +34,9 @@ angular.module('markdownFormatWdiffApp')
$scope.wdiff = result.wdiff;
$scope.revisionA = result.a;
$scope.revisionB = result.b;
+
+ $scope.title = result.a.document.title;
+ $scope.subtitle = "wdiff: "+result.a.created + " / " + result.b.created;
});
$scope.json = function (object) { return JSON.stringify(object, null, " "); };
diff --git a/client/app/document/wdiff/wdiff.jade b/client/app/document/wdiff/wdiff.jade
index 176a559..244b897 100644
--- a/client/app/document/wdiff/wdiff.jade
+++ b/client/app/document/wdiff/wdiff.jade
@@ -1,6 +1,6 @@
wdiff.jadenav(ng-include='"components/navbar/navbar.html"')
-nav(ng-include='"components/elements/header.html"', ng-onload='title = {{revision.created}}')
+nav(ng-include='"components/elements/header.html"')
.container
.row
@@ -8,11 +8,8 @@ nav(ng-include='"components/elements/header.html"', ng-onload='title = {{revisio
.row
.col-lg-6.col-md-9.center-block
- h4
- | wdiff {{a.created}} " -- " {{b.created}}
- div
div(btf-markdown='wdiff')
- pre
+ //pre
{{json(result)}}
diff --git a/client/components/elements/header.jade b/client/components/elements/header.jade
index 9554f5b..c4b7f2e 100644
--- a/client/components/elements/header.jade
+++ b/client/components/elements/header.jade
@@ -1,3 +1,4 @@
header#banner.hero-unit
.container
- h1 {{title}}
\ No newline at end of file
+ h1 {{title}}
+ h3 {{subtitle}}
\ No newline at end of file
diff --git a/package.json b/package.json
index 796fbbd..b04b41f 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"express-session": "~1.0.2",
"jade": "~1.2.0",
"jsonwebtoken": "^0.3.0",
+ "lex": "^1.7.8",
"lodash": "~2.4.1",
"method-override": "~1.0.0",
"mongoose": "~3.8.8",
diff --git a/server/api/document/document.controller.js b/server/api/document/document.controller.js
index 0f9df4a..e551fa8 100644
--- a/server/api/document/document.controller.js
+++ b/server/api/document/document.controller.js
@@ -22,6 +22,7 @@ exports.index = function(req, res) {
.find()
.populate('owner', '_id name')
.populate('currentRevision', '_id state created')
+ .populate('revisions', '_id state created description')
.exec(function (err, documents) {
if(err) { return handleError(res, err); }
return res.json(200, documents);
@@ -33,6 +34,7 @@ exports.indexForUser = function(req, res) {
Document
.find({owner: req.params.userid})
.populate('owner', '_id name')
+ .populate('revisions', '_id state created description')
.populate('currentRevision', '_id state created')
.exec(function (err, documents) {
if(err) { return handleError(res, err); }
@@ -189,21 +191,36 @@ exports.createRevision = function(req, res) {
//and the date
if (req.body.created) { delete req.body.created; }
+ //and the id!
+ if (req.body._id) { delete req.body._id; }
+
+ console.log(req.body);
+
//get the record for the parent document
Document.findById(req.params.id).exec(function(err, document){
- if (err) { return handleError(res, err); }
+ if (err) { return handleError(res, err); }
if(!document) { return res.send(404); }
// require user authentication
if (! mongoose.Types.ObjectId(document.owner).equals(req.user._id))
{return res.send(401);}
+ console.log('---');
+ console.log(document);
+
//set the owner and document fields for the revision
var revision = _.merge(req.body, {owner: req.user, document: document});
+ console.log('---');
+ console.log(revision);
+
//create the record
Revision.create(revision, function (err, revision) {
if(err) { return handleError(res, err); }
+ if (!revision) {return handleError(res, "Unknown error creating revision");}
+
+ console.log('---');
+ console.log(revision);
//and update the document
document.revisions.push(revision);
@@ -219,12 +236,18 @@ exports.createRevision = function(req, res) {
//compares two revisions with wdiff
exports.wdiff = function(req, res) {
- Revision.findById(req.params.revisionida).exec(function (err, revisiona) {
+ Revision
+ .findById(req.params.revisionida)
+ .populate('document', 'title')
+ .exec(function (err, revisiona) {
if(err) { return handleError(res, err); }
if(!revisiona) { return res.send(404); }
- Revision.findById(req.params.revisionidb).exec(function (err, revisionb) {
+ Revision
+ .findById(req.params.revisionidb)
+ .populate('document', 'title')
+ .exec(function (err, revisionb) {
if(err) { return handleError(res, err); }
if(!revisionb) { return res.send(404); }
diff --git a/server/components/wdiff/index.js b/server/components/wdiff/index.js
index cc1792e..e878b81 100644
--- a/server/components/wdiff/index.js
+++ b/server/components/wdiff/index.js
@@ -3,7 +3,8 @@
var _ = require('lodash'),
temp = require('temp'),
fs = require('fs'),
- exec = require('child_process').exec;
+ exec = require('child_process').exec,
+ Lexer = require('lex');
// Automatically track and cleanup files at exit
temp.track();
@@ -58,12 +59,9 @@ module.exports = function(a, b, asMarkdown, callback) {
if (asMarkdown) {
//!!! this needs more sophisticated parsing
- //sub del and ins for the wdiff tags
- var markdown = stdout;
- markdown = markdown.replace(/\[-/g, '');
- markdown = markdown.replace(/-\]/g, '');
- markdown = markdown.replace(/{\+/g, '');
- markdown = markdown.replace(/\+}/g, '');
+
+ var markdown = rewriteWdiffMarkdown(stdout)
+
resData.wdiff=markdown;
}
@@ -74,3 +72,178 @@ module.exports = function(a, b, asMarkdown, callback) {
});
});
}
+
+/* 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;
+
+ // 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]+[0-9]+\.))?[ \t]+/
+ //var PREFIX = /^#*/
+
+
+ transform.forEach(function(item) {
+ //newlines are undecorated
+ if (item.string == '\n') {
+ output += '\n';
+ newline = true;
+ return
+ }
+
+ var prestring = "";
+ var poststring = item.string;
+
+ //if this is a newline, we need to peel off any markdown formatting prefixes
+ //and output them outside the del/ins tags
+ if (newline) {
+ var match = item.string.match(PREFIX);
+ if (match == null)
+ prestring ="";
+ else
+ prestring = match[0];
+
+ poststring = item.string.substring(prestring.length);
+ }
+
+ //wrap ins and del strings with tags
+ if (item.state == SDEL)
+ output += prestring+'' + poststring + '';
+ else if (item.state ==SINS)
+ output += prestring+'' + poststring + '';
+
+ //and just output other strings
+ else
+ output += prestring+poststring;
+
+ newline = false;
+ });
+ return output;
+
+}
\ No newline at end of file
diff --git a/server/config/environment/development.js b/server/config/environment/development.js
index 39194d1..f4b1194 100644
--- a/server/config/environment/development.js
+++ b/server/config/environment/development.js
@@ -8,5 +8,5 @@ module.exports = {
uri: 'mongodb://mongodb/markdownformatwdiff-dev'
},
- seedDB: true
+ seedDB: false
};