much improvement to client side \n fixed wdiff output for markdown server side

This commit is contained in:
Adam Brown 2015-02-08 20:42:31 -05:00
parent 4917885686
commit ee3bd40ef7
18 changed files with 328 additions and 66 deletions

View File

@ -15,10 +15,21 @@ $fa-font-path: "/bower_components/font-awesome/fonts";
padding: 0.2em 0; 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 // Component styles are injected through grunt
// injector // injector
@import 'account/login/login.scss'; @import 'account/login/login.scss';
@import 'admin/admin.scss'; @import 'admin/admin.scss';
@import 'document/document.scss';
@import 'document/revision-new/revision-new.scss';
@import 'wdiff/wdiff.scss'; @import 'wdiff/wdiff.scss';
@import 'modal/modal.scss'; @import 'modal/modal.scss';
// endinjector // endinjector

View File

@ -0,0 +1,3 @@
table.revisions {
column-gap: 2em;
}

View File

@ -2,8 +2,10 @@
angular.module('markdownFormatWdiffApp') angular.module('markdownFormatWdiffApp')
.controller('DocumentIndexCtrl', function ($scope, $routeParams, $http, Auth, $location) { .controller('DocumentIndexCtrl', function ($scope, $routeParams, $http, Auth, $location) {
$scope.title = 'Documents';
$scope.documents = []; $scope.documents = [];
$scope.newDocumentTitle; $scope.newDocumentTitle = '';
$scope.getCurrentUser = Auth.getCurrentUser; $scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn; $scope.isLoggedIn = Auth.isLoggedIn;

View File

@ -1,10 +1,10 @@
nav(ng-include='"components/navbar/navbar.html"') 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 .container
.row(ng-show='isLoggedIn()') .row(ng-show='isLoggedIn()')
.col-lg-9.col-md-12.center-block .col-lg-9.col-md-12
form.form-inline form.form-inline
.form-group .form-group
label(for='title-input') label(for='title-input')
@ -15,17 +15,27 @@ nav(ng-include='"components/elements/header.html"', onload='title = "documents"'
.row .row
.col-lg-6.col-md-12(ng-repeat='document in documents | orderBy:"currentRevision.created":true' ) .col-lg-6.col-md-12(ng-repeat='document in documents | orderBy:"currentRevision.created":true' )
h4 h1
a(href='/{{document._id}}') a(href='/{{document._id}}')
{{document.title}} {{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 p
| State: | State:
{{document.currentRevision.state}} {{document.currentRevision.state}}
p p
| Updated: | Updated:
{{document.currentRevision.created}} {{document.currentRevision.created}}
pre //pre
{{json(document)}} {{json(document)}}

View File

@ -2,8 +2,12 @@
angular.module('markdownFormatWdiffApp') angular.module('markdownFormatWdiffApp')
.controller('DocumentRevisionNewCtrl', function ($scope, $routeParams, $http, Auth, $location) { .controller('DocumentRevisionNewCtrl', function ($scope, $routeParams, $http, Auth, $location) {
$scope.title = '';
$scope.subtitle = '';
$scope.revision = {}; $scope.revision = {};
$scope.stateOptions = ['first draft', 'final draft', 'first edit', 'final edit'];
$scope.getCurrentUser = Auth.getCurrentUser; $scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn; $scope.isLoggedIn = Auth.isLoggedIn;
@ -17,13 +21,16 @@ angular.module('markdownFormatWdiffApp')
}; };
var path = '/api/documents/' + $routeParams.id; var path = '/api/documents/' + $routeParams.id;
$http.get(path).success(function(revision) { $http.get(path).success(function(document) {
$scope.document = document; $scope.document = document;
$scope.revision = angular.copy(document.currentRevision); $scope.revision = angular.copy(document.currentRevision);
$scope.title = document.title;
$scope.subtitle = 'new revision';
}); });
$scope.saveRevision = function() { $scope.saveRevision = function() {
alert(JSON.stringify($scope.revision))
//save the revision to the document //save the revision to the document
$http.post('/api/documents/'+$routeParams.id+'/revisions', $scope.revision) $http.post('/api/documents/'+$routeParams.id+'/revisions', $scope.revision)
.success(function(newRevision) { .success(function(newRevision) {

View File

@ -1,24 +1,30 @@
nav(ng-include='"components/navbar/navbar.html"') 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 .container
.row .row
form.col-lg-9.col-md-12.center-block .col-lg-6.col-md-12
h4 form
{{revision.document.title}} - {{revision.created}} h4
.form-group {{document.title}} - {{revision.created}}
label(for='status-input') .form-group
| Status label(for='status-input')
input.form-control(type='text', id='status-input', ng-model='revision.status') | Status
.form-group select.form-control(id='status-input', ng-model='revision.state', ng-options='stateOption as stateOption for stateOption in stateOptions')
label(for='content-input') option(value='{{stateOption}}')
| Content //input.form-control(type='text', id='status-input', ng-model='revision.state')
textarea.form-control(id='content-input', ng-model='revision.content') .form-group
button.btn.btn-default(ng-click='saveRevision()') label(for='content-input')
| Save | 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 pre.col-lg-9.col-md-12.center-block
{{json(revision)}} {{json(revision)}}

View File

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

View File

@ -4,6 +4,9 @@ angular.module('markdownFormatWdiffApp')
.controller('DocumentRevisionCtrl', function ($scope, $routeParams, $http, Auth) { .controller('DocumentRevisionCtrl', function ($scope, $routeParams, $http, Auth) {
$scope.revision = {}; $scope.revision = {};
$scope.title = '';
$scope.subtitle = '';
$scope.getCurrentUser = Auth.getCurrentUser; $scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn; $scope.isLoggedIn = Auth.isLoggedIn;
@ -27,6 +30,8 @@ angular.module('markdownFormatWdiffApp')
var path = '/api/documents/'+$routeParams.id+'/revisions/' + $routeParams.revisionid; var path = '/api/documents/'+$routeParams.id+'/revisions/' + $routeParams.revisionid;
$http.get(path).success(function(revision) { $http.get(path).success(function(revision) {
$scope.revision = 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, " "); }; $scope.json = function (object) { return JSON.stringify(object, null, " "); };

View File

@ -1,27 +1,22 @@
nav(ng-include='"components/navbar/navbar.html"') 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 .container
.row .row
span(ng-show='isCurrent()') //span(ng-show='isCurrent()')
| Current revision | Current revision
a.btn.btn-primary(ng-hide='isCurrent()' href='/wdiff/{{revision._id}}/{{revision.document.currentRevision}}') a.btn.btn-primary(ng-hide='isCurrent()' href='/wdiff/{{revision._id}}/{{revision.document.currentRevision}}')
| wdiff current | wdiff current
.row .row
.col-lg-6.col-md-9.center-block .col-lg-6.col-md-9
h4
{{revision.created}}
div div
p p
| State: | State:
{{revision.state}} {{revision.state}}
p
| Updated:
{{revision.created}}
div(btf-markdown='revision.content') div(btf-markdown='revision.content')
pre //pre
{{json(revision)}} {{json(revision)}}

View File

@ -3,6 +3,7 @@
angular.module('markdownFormatWdiffApp') angular.module('markdownFormatWdiffApp')
.controller('DocumentShowCtrl', function ($scope, $routeParams, $http, Auth) { .controller('DocumentShowCtrl', function ($scope, $routeParams, $http, Auth) {
$scope.document = {}; $scope.document = {};
$scope.title = '';
$scope.getCurrentUser = Auth.getCurrentUser; $scope.getCurrentUser = Auth.getCurrentUser;
$scope.isLoggedIn = Auth.isLoggedIn; $scope.isLoggedIn = Auth.isLoggedIn;
@ -19,6 +20,7 @@ angular.module('markdownFormatWdiffApp')
var path = '/api/documents/' + $routeParams.id; var path = '/api/documents/' + $routeParams.id;
$http.get(path).success(function(document) { $http.get(path).success(function(document) {
$scope.document = document; $scope.document = document;
$scope.title = document.title;
}); });
$scope.json = function (object) { return JSON.stringify(object, null, " "); }; $scope.json = function (object) { return JSON.stringify(object, null, " "); };

View File

@ -1,26 +1,28 @@
nav(ng-include='"components/navbar/navbar.html"') 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 .container
.row(ng-show='isOwner()') .row(ng-show='isOwner()')
form.col-lg-12 .col-lg-12
a.btn.btn-primary(href='/{{document._id}}/revision/new') form
| New Revision 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 .col-lg-6.col-md-9.center-block
h4
a(href='/{{document._id}}/revision/{{revision._id}}') table.revisions
{{revision.created}} tr
div th.state State
p th.created revision
| State: tr(ng-repeat='revision in document.revisions | orderBy:"created":true | limitTo:5' )
{{revision.state}} td.state {{revision.state}}
p td.created
| Updated: h4
{{revision.created}} a(href='/{{document._id}}/revision/{{revision._id}}')
pre {{revision.created}}
//pre
| {{json(revision)}} | {{json(revision)}}

View File

@ -2,7 +2,8 @@
angular.module('markdownFormatWdiffApp') angular.module('markdownFormatWdiffApp')
.controller('DocumentWdiffCtrl', function ($scope, $routeParams, $http, Auth) { .controller('DocumentWdiffCtrl', function ($scope, $routeParams, $http, Auth) {
$scope.wdiff = ""; $scope.wdiff = '';
$scope.title = '';
$scope.same = false; $scope.same = false;
$scope.revisionA = {}; $scope.revisionA = {};
$scope.revisionB = {}; $scope.revisionB = {};
@ -33,6 +34,9 @@ angular.module('markdownFormatWdiffApp')
$scope.wdiff = result.wdiff; $scope.wdiff = result.wdiff;
$scope.revisionA = result.a; $scope.revisionA = result.a;
$scope.revisionB = result.b; $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, " "); }; $scope.json = function (object) { return JSON.stringify(object, null, " "); };

View File

@ -1,6 +1,6 @@
wdiff.jadenav(ng-include='"components/navbar/navbar.html"') 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 .container
.row .row
@ -8,11 +8,8 @@ nav(ng-include='"components/elements/header.html"', ng-onload='title = {{revisio
.row .row
.col-lg-6.col-md-9.center-block .col-lg-6.col-md-9.center-block
h4
| wdiff {{a.created}} " -- " {{b.created}}
div
div(btf-markdown='wdiff') div(btf-markdown='wdiff')
pre //pre
{{json(result)}} {{json(result)}}

View File

@ -1,3 +1,4 @@
header#banner.hero-unit header#banner.hero-unit
.container .container
h1 {{title}} h1 {{title}}
h3 {{subtitle}}

View File

@ -14,6 +14,7 @@
"express-session": "~1.0.2", "express-session": "~1.0.2",
"jade": "~1.2.0", "jade": "~1.2.0",
"jsonwebtoken": "^0.3.0", "jsonwebtoken": "^0.3.0",
"lex": "^1.7.8",
"lodash": "~2.4.1", "lodash": "~2.4.1",
"method-override": "~1.0.0", "method-override": "~1.0.0",
"mongoose": "~3.8.8", "mongoose": "~3.8.8",

View File

@ -22,6 +22,7 @@ exports.index = function(req, res) {
.find() .find()
.populate('owner', '_id name') .populate('owner', '_id name')
.populate('currentRevision', '_id state created') .populate('currentRevision', '_id state created')
.populate('revisions', '_id state created description')
.exec(function (err, documents) { .exec(function (err, documents) {
if(err) { return handleError(res, err); } if(err) { return handleError(res, err); }
return res.json(200, documents); return res.json(200, documents);
@ -33,6 +34,7 @@ exports.indexForUser = function(req, res) {
Document Document
.find({owner: req.params.userid}) .find({owner: req.params.userid})
.populate('owner', '_id name') .populate('owner', '_id name')
.populate('revisions', '_id state created description')
.populate('currentRevision', '_id state created') .populate('currentRevision', '_id state created')
.exec(function (err, documents) { .exec(function (err, documents) {
if(err) { return handleError(res, err); } if(err) { return handleError(res, err); }
@ -189,21 +191,36 @@ exports.createRevision = function(req, res) {
//and the date //and the date
if (req.body.created) { delete req.body.created; } 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 //get the record for the parent document
Document.findById(req.params.id).exec(function(err, 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); } if(!document) { return res.send(404); }
// require user authentication // require user authentication
if (! mongoose.Types.ObjectId(document.owner).equals(req.user._id)) if (! mongoose.Types.ObjectId(document.owner).equals(req.user._id))
{return res.send(401);} {return res.send(401);}
console.log('---');
console.log(document);
//set the owner and document fields for the revision //set the owner and document fields for the revision
var revision = _.merge(req.body, {owner: req.user, document: document}); var revision = _.merge(req.body, {owner: req.user, document: document});
console.log('---');
console.log(revision);
//create the record //create the record
Revision.create(revision, function (err, revision) { Revision.create(revision, function (err, revision) {
if(err) { return handleError(res, err); } if(err) { return handleError(res, err); }
if (!revision) {return handleError(res, "Unknown error creating revision");}
console.log('---');
console.log(revision);
//and update the document //and update the document
document.revisions.push(revision); document.revisions.push(revision);
@ -219,12 +236,18 @@ exports.createRevision = function(req, res) {
//compares two revisions with wdiff //compares two revisions with wdiff
exports.wdiff = function(req, res) { 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(err) { return handleError(res, err); }
if(!revisiona) { return res.send(404); } 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(err) { return handleError(res, err); }
if(!revisionb) { return res.send(404); } if(!revisionb) { return res.send(404); }

View File

@ -3,7 +3,8 @@
var _ = require('lodash'), var _ = require('lodash'),
temp = require('temp'), temp = require('temp'),
fs = require('fs'), fs = require('fs'),
exec = require('child_process').exec; exec = require('child_process').exec,
Lexer = require('lex');
// Automatically track and cleanup files at exit // Automatically track and cleanup files at exit
temp.track(); temp.track();
@ -58,12 +59,9 @@ module.exports = function(a, b, asMarkdown, callback) {
if (asMarkdown) { if (asMarkdown) {
//!!! this needs more sophisticated parsing //!!! this needs more sophisticated parsing
//sub del and ins for the wdiff tags
var markdown = stdout; var markdown = rewriteWdiffMarkdown(stdout)
markdown = markdown.replace(/\[-/g, '<del>');
markdown = markdown.replace(/-\]/g, '</del>');
markdown = markdown.replace(/{\+/g, '<ins>');
markdown = markdown.replace(/\+}/g, '</ins>');
resData.wdiff=markdown; 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+'<del>' + poststring + '</del>';
else if (item.state ==SINS)
output += prestring+'<ins>' + poststring + '</ins>';
//and just output other strings
else
output += prestring+poststring;
newline = false;
});
return output;
}

View File

@ -8,5 +8,5 @@ module.exports = {
uri: 'mongodb://mongodb/markdownformatwdiff-dev' uri: 'mongodb://mongodb/markdownformatwdiff-dev'
}, },
seedDB: true seedDB: false
}; };