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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

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) {
$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, " "); };

View File

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

View File

@ -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, " "); };

View File

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

View File

@ -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, " "); };

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -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, '<del>');
markdown = markdown.replace(/-\]/g, '</del>');
markdown = markdown.replace(/{\+/g, '<ins>');
markdown = markdown.replace(/\+}/g, '</ins>');
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+'<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'
},
seedDB: true
seedDB: false
};