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