Merge branch 'master' of github.com:sharelatex/web-sharelatex

This commit is contained in:
Henry Oswald 2014-03-08 19:01:21 +00:00
commit e7602321e9
25 changed files with 748 additions and 1948 deletions

View file

@ -84,6 +84,7 @@ module.exports = (grunt) ->
paths:
"underscore": "libs/underscore"
"jquery": "libs/jquery"
"moment": "libs/moment"
shim:
"libs/backbone":
deps: ["libs/underscore"]

View file

@ -217,7 +217,7 @@ module.exports = ProjectEntityHandler =
logger.err "error putting doc #{doc_id} in project #{project_id} #{err}"
callback err
else if docComparitor.areSame docLines, doc.lines
logger.log sl_req_id: sl_req_id, docLines:docLines, project_id:project_id, doc_id:doc_id, rev:doc.rev, "old doc lines are same as the new doc lines, not updating them"
logger.log sl_req_id: sl_req_id, project_id:project_id, doc_id:doc_id, rev:doc.rev, "old doc lines are same as the new doc lines, not updating them"
callback()
else
logger.log sl_req_id: sl_req_id, project_id:project_id, doc_id:doc_id, docLines: docLines, oldDocLines: doc.lines, rev:doc.rev, "updating doc lines"

View file

@ -0,0 +1,13 @@
logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
module.exports = TrackChangesController =
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
url = settings.apis.trackchanges.url + req.url
logger.log url: url, "proxying to track-changes api"
getReq = request.get(url)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"
next(error)

View file

@ -32,6 +32,7 @@ CompileController = require("./Features/Compile/CompileController")
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
logger = require("logger-sharelatex")
httpAuth = require('express').basicAuth (user, pass)->
@ -122,6 +123,9 @@ module.exports = class Router
app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
app.get "/project/:Project_id/doc/:doc_id/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.get "/project/:Project_id/doc/:doc_id/diff", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators

View file

@ -24,7 +24,7 @@ block content
p(style="clear: both")
h3 Motivation
p Our first priority with ShareLaTeX is to build a tool which makes life easier for all the LaTeX users our there.
p Our first priority with ShareLaTeX is to build a tool which makes life easier for all the LaTeX users out there.
| The "thank you"s and success stories are a strong motivator for us, and we hope to always be able to offer a fully-functional
| LaTeX editor which anyone can use for free.
p We also believe that charging money for tools like ShareLaTeX is important since it helps to guarantee the future of the site and

View file

@ -38,7 +38,8 @@ block content
window.requirejs = {
"paths" : {
"underscore": "libs/underscore",
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML",
"moment": "libs/moment"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}",
"waitSeconds": 0,

View file

@ -144,6 +144,12 @@ block content
mixin tag('{{ project_id }}', '{{ tagName }}', true)
- locals.supressDefaultJs = true
script
window.requirejs = {
"paths" : {
"moment": "libs/moment"
}
};
script(
data-main=jsPath+'list.js?fingerprint='+fingerprint(jsPath + 'list.js'),
baseurl=jsPath,

View file

@ -147,10 +147,11 @@
#editorArea(style='display: none;')
#editorSplitter
#leftEditorPanel.ui-layout-center
#editor
#undoConflictWarning(style="display: none")
| <strong>Watch out!</strong> We had to undo some of your collaborators changes before we could undo yours.
a(href="#").js-hide Hide
#editorWrapper
#editor
#undoConflictWarning(style="display: none")
| <strong>Watch out!</strong> We had to undo some of your collaborators changes before we could undo yours.
a(href="#").js-hide Hide
#rightEditorPanel.ui-layout-east
script(type="text/template")#loadingIndicatorTemplate
@ -403,11 +404,6 @@
div(class='version-message') {{message}}
div(class='version-date') {{date}}
script(type='text/template')#autoCompleteSuggestionTemplate
li
strong {{base}}
| {{completion}}
script(type='text/template')#versionListTemplate
ul#version-list.nav.nav-pills.nav-stacked
li.loading Loading...
@ -427,6 +423,32 @@
div
a(href="#", title='Show Hot Keys List') Hot keys
script(type='text/template')#trackChangesPanelTemplate
#trackChangesPanel
.track-changes-side-bar
.track-changes-header
h3 Recent changes
a(href="#").track-changes-close
i.icon-remove
.change-list-area
.track-changes-diff
script(type='text/template')#changeListItemTemplate
div(class='change-selectors')
input(type="radio",name="fromVersion").change-selector-from
input(type="radio",name="toVersion").change-selector-to
div(class='change-description')
div(class='change-date') {{date}}
div(class='change-name')
div.color-square(style="background-color: hsl({{hue}}, 100%, 70%);")
span {{name}}
script(type='text/template')#changeListTemplate
ul.change-list.nav.nav-pills.nav-stacked
li.loading-changes Loading...
li.empty-message You haven't made any changes yet!
script(type='text/template')#hotKeysListTemplate
.hotkeys
h3 Common

View file

@ -73,7 +73,7 @@ define [
if @currentViewState != @viewOptions.splitView
@currentViewState = @viewOptions.splitView
@leftPanel.prepend(
@editorPanel.find("#editor")
@editorPanel.find("#editorWrapper")
)
splitter = @editorPanel.find("#editorSplitter")
splitter.show()
@ -84,7 +84,7 @@ define [
@_saveSplitterState()
@currentViewState = @viewOptions.flatView
@editorPanel.prepend(
@editorPanel.find("#editor")
@editorPanel.find("#editorWrapper")
)
@editorPanel.find("#editorSplitter").hide()
@aceEditor.resize(true)
@ -352,3 +352,9 @@ define [
getCurrentDocId: () ->
@current_doc_id
show: () ->
$("#editor").show()
hide: () ->
$("#editor").hide()

View file

@ -25,6 +25,7 @@ define [
"file-view/FileViewManager"
"tour/IdeTour"
"analytics/AnalyticsManager"
"track-changes/TrackChangesManager"
"ace/ace"
"libs/jquery.color"
"libs/jquery-layout"
@ -56,7 +57,8 @@ define [
BackspaceHighjack,
FileViewManager,
IdeTour,
AnalyticsManager
AnalyticsManager,
TrackChangesManager
) ->
@ -117,6 +119,7 @@ define [
@cursorManager = new CursorManager(@)
@fileViewManager = new FileViewManager(@)
@analyticsManager = new AnalyticsManager(@)
@trackChangesManager = new TrackChangesManager(@)
@setLoadingMessage("Connecting")
firstConnect = true

View file

@ -1,16 +1,16 @@
require [
"tags"
"moment"
"gui"
"libs/moment"
"libs/underscore"
"libs/fineuploader"
"libs/jquery.storage"
], (tagsManager)->
], (tagsManager, moment)->
$('.isoDate').each (i, d)->
html = $(d)
unparsedDate = html.text()
formatedDate = moment(unparsedDate).format('LLL')
unparsedDate = html.text().trim()
formatedDate = moment(unparsedDate).format("Do MMM YYYY, h:mm:ss a")
html.text(formatedDate)
refreshProjectFilter = ->

View file

@ -1,7 +1,38 @@
define [
"libs/md5"
"libs/backbone"
], () ->
User = Backbone.Model.extend {}, {
User = Backbone.Model.extend {
gravatarUrl: (size = 32) ->
email = @get("email").trim().toLowerCase()
hash = CryptoJS.MD5(email)
return "//www.gravatar.com/avatar/#{hash}.jpg?size=#{size}&d=mm"
OWNER_HUE: 200
hue: () ->
if window.user.id == @get("id")
hue = @OWNER_HUE
else
hash = CryptoJS.MD5(@get("id"))
hue = parseInt(hash.toString().slice(0,8), 16) % 320
# Avoid 20 degrees either side of the owner
if hue > @OWNER_HUE - 20
hue = hue + 40
return hue
name: () ->
if window.user.id == @get("id")
return "you"
parts = []
first_name = @get("first_name")
if first_name? and first_name.length > 0
parts.push first_name
last_name = @get("last_name")
if last_name? and last_name.length > 0
parts.push last_name
return parts.join(" ")
}, {
findOrBuild : (id, attributes) ->
model = @find id
if !model?

View file

@ -0,0 +1,198 @@
define [
"moment"
"libs/mustache"
"libs/backbone"
], (moment)->
ChangeListView = Backbone.View.extend
template: $("#changeListTemplate").html()
events:
"scroll" : "loadUntilFull"
initialize: () ->
@itemViews = []
@atEndOfCollection = false
self = this
@collection.on "add", (model) ->
self.addItem model
@collection.on "reset", (collection) ->
self.addItem model for model in collection.models
@selectedFromIndex = 0
@selectedToIndex = 0
@render()
@hideLoading()
render: ->
@$el.html Mustache.to_html @template
@$el.css
overflow: "scroll"
this
addItem: (model) ->
index = @collection.indexOf(model)
view = new ChangeListItemView(model : model)
@itemViews.push view
elementAtIndex = @$(".change-list").children()[index]
view.$el.insertBefore(elementAtIndex)
view.on "click", (e, v) =>
@selectedToIndex = index
@selectedFromIndex = index
@resetAllSelectors()
@triggerChangeDiff()
view.on "selected:to", (e, v) =>
@selectedToIndex = index
@resetAllSelectors()
@triggerChangeDiff()
view.on "selected:from", (e, v) =>
@selectedFromIndex = index
@resetAllSelectors()
@triggerChangeDiff()
view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
resetAllSelectors: () ->
for view, i in @itemViews
view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
triggerChangeDiff: () ->
@trigger "change_diff", @collection.models[@selectedFromIndex], @collection.models[@selectedToIndex]
listShorterThanContainer: ->
@$el.height() > @$(".change-list").height()
atEndOfListView: ->
@$el.scrollTop() + @$el.height() >= @$(".change-list").height() - 30
loadUntilFull: (e, callback) ->
if (@listShorterThanContainer() or @atEndOfListView()) and not @atEndOfCollection and not @loading
@showLoading()
@hideEmptyMessage()
@collection.fetchNextBatch
error: =>
@hideLoading()
@showEmptyMessageIfCollectionEmpty()
callback() if callback?
success: (collection, response) =>
@hideLoading()
if response.updates.length == @collection.batchSize
@loadUntilFull(e, callback)
else
@atEndOfCollection = true
@showEmptyMessageIfCollectionEmpty()
callback() if callback?
else
callback() if callback?
showEmptyMessageIfCollectionEmpty: ()->
if @collection.isEmpty()
@$(".empty-message").show()
else
@$(".empty-message").hide()
hideEmptyMessage: () ->
@$(".empty-message").hide()
showLoading: ->
@loading = true
@$(".loading-changes").show()
hideLoading: ->
@loading = false
@$(".loading-changes").hide()
ChangeListItemView = Backbone.View.extend
tagName: "li"
events:
"click .change-description" : "onClick"
"click .change-selector-from" : "onFromSelectorClick"
"click .change-selector-to" : "onToSelectorClick"
template : $("#changeListItemTemplate").html()
initialize: ->
@render()
render: ->
@$el.html Mustache.to_html(@template, @modelView())
return this
modelView: ->
modelView = {
hue: @model.get("user").hue()
date: moment(parseInt(@model.get("end_ts"), 10)).calendar()
name: @model.get("user").name()
}
# modelView.start_ts = util.formatDate(modelView.start_ts)
# modelView.end_ts = util.formatDate(modelView.end_ts)
return modelView
onClick: (e) ->
e.preventDefault()
@trigger "click", e, @
onToSelectorClick: (e) ->
@trigger "selected:to", e, @
onFromSelectorClick: (e) ->
@trigger "selected:from", e, @
isSelectedFrom: () ->
@$(".change-selector-from").is(":checked")
isSelectedTo: () ->
@$(".change-selector-to").is(":checked")
hideFromSelector: () ->
@$(".change-selector-from").hide()
showFromSelector: () ->
@$(".change-selector-from").show()
hideToSelector: () ->
@$(".change-selector-to").hide()
showToSelector: () ->
@$(".change-selector-to").show()
setFromChecked: (checked) ->
@$(".change-selector-from").prop("checked", checked)
setToChecked: (checked) ->
@$(".change-selector-to").prop("checked", checked)
setSelected: () ->
@$el.addClass("selected")
setUnselected: () ->
@$el.removeClass("selected")
resetSelector: (myIndex, selectedFromIndex, selectedToIndex) ->
if myIndex >= selectedToIndex
@showFromSelector()
else
@hideFromSelector()
if myIndex <= selectedFromIndex
@showToSelector()
else
@hideToSelector()
if selectedToIndex <= myIndex <= selectedFromIndex
@setSelected()
else
@setUnselected()
@setFromChecked(myIndex == selectedFromIndex)
@setToChecked(myIndex == selectedToIndex)
return ChangeListView

View file

@ -0,0 +1,140 @@
define [
"ace/ace"
"ace/mode/latex"
"ace/range"
"libs/backbone"
], (Ace, LatexMode, Range)->
DiffView = Backbone.View.extend
initialize: () ->
@model.on "change:diff", () => @render()
@render()
render: ->
diff = @model.get("diff")
return unless diff?
@createAceEditor()
@aceEditor.setValue(@getPlainDiffContent())
@aceEditor.clearSelection()
session = @aceEditor.getSession()
session.setMode(new LatexMode.Mode())
session.setUseWrapMode(true)
@insertMarkers()
return @
createAceEditor: () ->
@$el.empty()
$editor = $("<div/>")
@$el.append($editor)
@aceEditor = Ace.edit($editor[0])
@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
@aceEditor.setReadOnly true
@aceEditor.setShowPrintMargin(false)
@aceEditor.on "mousemove", (e) =>
position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
e.position = position
@updateVisibleNames(e)
getPlainDiffContent: () ->
content = ""
for entry in @model.get("diff") or []
content += entry.u or entry.i or entry.d or ""
return content
insertMarkers: () ->
row = 0
column = 0
for entry, i in @model.get("diff") or []
content = entry.u or entry.i or entry.d
lines = content.split("\n")
startRow = row
startColumn = column
if lines.length > 1
endRow = startRow + lines.length - 1
endColumn = lines[lines.length - 1].length
else
endRow = startRow
endColumn = startColumn + lines[0].length
row = endRow
column = endColumn
range = new Range.Range(
startRow, startColumn, endRow, endColumn
)
@addMarker(range, "change-marker-#{i}", entry)
addMarker: (range, id, entry) ->
session = @aceEditor.getSession()
markerBackLayer = @aceEditor.renderer.$markerBack
markerFrontLayer = @aceEditor.renderer.$markerFront
lineHeight = @aceEditor.renderer.lineHeight
if entry.i? or entry.d?
hue = entry.meta.user.hue()
if entry.i?
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
background-color : hsl(#{hue}, 70%, 85%);
"""
tag = "Added by #{entry.meta.user.name()}"
if entry.d?
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
background-color : hsl(#{hue}, 70%, 95%);
"""
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, """
height: #{Math.round(lineHeight/2) - 1}px;
border-bottom: 2px solid hsl(#{hue}, 70%, 40%);
"""
tag = "Deleted by #{entry.meta.user.name()}"
date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm:ss a")
tag += " on #{date}"
@_addNameTag session, id, range, tag, """
background-color : hsl(#{hue}, 70%, 95%);
"""
_addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
session.addMarker range, klass, (html, range, left, top, config) ->
if range.isMultiLine()
markerLayer.drawTextMarker(html, range, klass, config, style)
else
markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
, foreground
_addNameTag: (session, id, range, content, style) ->
@nameMarkers ||= []
@nameMarkers.push
range: range
id: id
startRange = new Range.Range(
range.start.row, range.start.column
range.start.row, range.start.column + 1
)
session.addMarker startRange, "change-name-marker", (html, range, left, top, config) ->
html.push """
<div
id = '#{id}'
class = 'change-name-marker'
style = '
height: #{config.lineHeight}px;
top: #{top}px;
left: #{left}px;
'
>
<div
class="name" style="
display: none;
bottom: #{config.lineHeight + 3}px;
#{style}
">#{content}</div>
</div>
"""
, true
updateVisibleNames: (e) ->
for marker in @nameMarkers or []
if marker.range.contains(e.position.row, e.position.column)
$("##{marker.id}").find(".name").show()
else
$("##{marker.id}").find(".name").hide()
return DiffView

View file

@ -0,0 +1,57 @@
define [
"track-changes/models/ChangeList"
"track-changes/models/Diff"
"track-changes/ChangeListView"
"track-changes/DiffView"
], (ChangeList, Diff, ChangeListView, DiffView) ->
class TrackChangesManager
template: $("#trackChangesPanelTemplate").html()
constructor: (@ide) ->
@$el = $(@template)
$("#editorWrapper").append(@$el)
@hideEl()
@ide.editor.on "change:doc", () =>
@hideEl()
@$el.find(".track-changes-close").on "click", (e) =>
e.preventDefault
@hideEl()
show: () ->
@project_id = window.userSettings.project_id
@doc_id = @ide.editor.current_doc_id
@changes = new ChangeList([], doc_id: @doc_id, project_id: @project_id)
@changeListView = new ChangeListView(
collection : @changes,
el : @$el.find(".change-list-area")
)
@changeListView.render()
@changeListView.loadUntilFull()
@changeListView.on "change_diff", (fromModel, toModel) =>
@diff = new Diff({
project_id: @project_id
doc_id: @doc_id
from: fromModel.get("version")
to: toModel.get("version")
})
@diffView = new DiffView(
model: @diff
el: @$el.find(".track-changes-diff")
)
@diff.fetch()
@showEl()
showEl: ->
@ide.editor.hide()
@$el.show()
hideEl: () ->
@ide.editor.show()
@$el.hide()
return TrackChangesManager

View file

@ -0,0 +1,12 @@
define [
"models/User"
"libs/backbone"
], (User)->
Change = Backbone.Model.extend
parse: (change) ->
return {
start_ts: change.meta.start_ts
end_ts: change.meta.end_ts
user: User.findOrBuild(change.meta.user.id, change.meta.user)
version: change.v
}

View file

@ -0,0 +1,25 @@
define [
"track-changes/models/Change"
"libs/backbone"
], (Change)->
ChangeList = Backbone.Collection.extend
model: Change
batchSize: 25
initialize: (models, @options) ->
url: () ->
url = "/project/#{@options.project_id}/doc/#{@options.doc_id}/updates?limit=#{@batchSize}"
if @models.length > 0
last = @models[@models.length - 1]
url += "&to=#{last.get("version") - 1}"
return url
parse: (json) ->
return json.updates
fetchNextBatch: (options = {}) ->
options.add = true
@fetch options

View file

@ -0,0 +1,13 @@
define [
"models/User"
"libs/backbone"
], (User) ->
Diff = Backbone.Model.extend
url: () ->
"/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff?from=#{@get("from")}&to=#{@get("to")}"
parse: (diff) ->
for entry in diff.diff
if entry.meta? and entry.meta.user?
entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
return diff

View file

@ -0,0 +1,19 @@
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(s,p){var m={},l=m.lib={},n=function(){},r=l.Base={extend:function(b){n.prototype=this;var h=new n;b&&h.mixIn(b);h.hasOwnProperty("init")||(h.init=function(){h.$super.init.apply(this,arguments)});h.init.prototype=h;h.$super=this;return h},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var h in b)b.hasOwnProperty(h)&&(this[h]=b[h]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}},
q=l.WordArray=r.extend({init:function(b,h){b=this.words=b||[];this.sigBytes=h!=p?h:4*b.length},toString:function(b){return(b||t).stringify(this)},concat:function(b){var h=this.words,a=b.words,j=this.sigBytes;b=b.sigBytes;this.clamp();if(j%4)for(var g=0;g<b;g++)h[j+g>>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535<a.length)for(g=0;g<b;g+=4)h[j+g>>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<<
32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a<b;a+=4)h.push(4294967296*s.random()|0);return new q.init(h,b)}}),v=m.enc={},t=v.Hex={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++){var k=a[j>>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j+=2)g[j>>>3]|=parseInt(b.substr(j,
2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++)g.push(String.fromCharCode(a[j>>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j++)g[j>>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}},
g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l<b;l+=k)this._doProcessBlock(g,l);l=g.splice(0,b);a.sigBytes-=j}return new q.init(l,j)},clone:function(){var b=r.clone.call(this);
b._data=this._data.clone();return b},_minBufferSize:0});l.Hasher=g.extend({cfg:r.extend(),init:function(b){this.cfg=this.cfg.extend(b);this.reset()},reset:function(){g.reset.call(this);this._doReset()},update:function(b){this._append(b);this._process();return this},finalize:function(b){b&&this._append(b);return this._doFinalize()},blockSize:16,_createHelper:function(b){return function(a,g){return(new b.init(g)).finalize(a)}},_createHmacHelper:function(b){return function(a,g){return(new k.HMAC.init(b,
g)).finalize(a)}}});var k=m.algo={};return m}(Math);
(function(s){function p(a,k,b,h,l,j,m){a=a+(k&b|~k&h)+l+m;return(a<<j|a>>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<<j|a>>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<<j|a>>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<<j|a>>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])},
_doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]),
c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c,
d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d,
C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/
4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math);

File diff suppressed because one or more lines are too long

View file

@ -177,25 +177,13 @@ body.editor {
width: 100%;
height: 100%;
}
#editor, #orchard {
#editor {
width: 100%;
height: 100%;
}
#orchard-toolbar {
height: 28px;
padding: 5px;
background-color: white;
border-bottom: 1px solid #aaa;
select {
margin-bottom: 0;
}
}
#orchard-editor {
position: absolute;
top: 39px;
bottom: 0;
left: 0;
right: 0;
#editorWrapper {
height: 100%;
width: 100%;
}
#rightEditorPanel {
border-left: 1px solid #aaa;
@ -212,6 +200,10 @@ body.editor {
bottom: 0;
z-index: 100;
}
#trackChangesPanel {
width: 100%;
height: 100%;
}
}
#undoConflictWarning {
@ -964,18 +956,6 @@ i[class*="sprite-"] {
background-color: #9a2b1f \9;
}
#orchard-editor {
background: white;
> .CodeMirror > .CodeMirror-scroll > .CodeMirror-sizer > div > .CodeMirror-lines {
background-color: white;
margin: auto;
max-width: 390pt;
min-height: 592pt;
margin-top: 20px;
margin-bottom: 20px;
}
}
#errorMessages {
z-index: 10000;
top: 4px;
@ -998,5 +978,3 @@ i[class*="sprite-"] {
margin-bottom: 6px;
}
}
@import "./orchard";

View file

@ -1,236 +0,0 @@
.editor-font {
font-size: 11pt;
line-height: 1.4em;
font-family: "Computer Modern", serif;
font-weight: normal;
}
@font-face {
font-family: "Computer Modern";
src: url('../font/cmunrm.otf');
}
@font-face {
font-family: "Computer Modern";
src: url('../font/cmunrb.otf');
font-weight: bold;
}
@font-face {
font-family: "Computer Modern Monospace";
src: url('../font/cmuntt.otf');
font-weight: bold;
}
.CodeMirror {
.CodeMirror-placeholder {
color: #999;
}
height: auto;
background: none;
.editor-font;
.cm-command, .cm-bracket {
color: green;
}
.cm-math {
color: purple;
font-family: "Computer Modern Monospace", monospace;
}
.CodeMirror-lines {
padding: 10px;
}
.oe-chapter {
font-weight: bold;
font-size: 1.8em !important;
line-height: 2em !important;
}
.oe-section {
font-weight: bold;
font-size: 1.5em !important;
line-height: 2em !important;
}
.oe-subsection {
font-weight: bold;
font-size: 1.3em !important;
line-height: 2em !important;
}
.oe-widget-line {
display: none;
}
.oe-figure {
margin: 0 5px;
padding: 12px 0 4px 0;
text-align: center;
position: relative;
.ui-wrapper {
position: relative;
display: inline-block;
}
.oe-figure-image-wrapper {
display: inline-block;
}
.oe-figure-image-placeholder {
background-color: rgb(200,200,200);
}
.oe-figure-image-placeholder.droppable-hover {
background-color: rgb(200,200,255);
}
.oe-figure-toolbar {
display: none;
text-align: right;
padding: 4px;
position: absolute;
top: 0;
left: 0;
right: 0;
}
}
.oe-title {
.CodeMirror-lines {
padding: 0;
}
.oe-title-title {
text-align: center;
margin-top: 40px;
margin-bottom: 20px;
.CodeMirror {
font-size: 1.6em;
line-height: 1.3em;
}
}
.oe-title-author {
text-align: center;
margin-bottom: 10px;
.CodeMirror {
font-size: 1.2em;
line-height: 1.3em;
}
}
.oe-title-abstract {
margin-top: 40px;
margin-bottom: 30px;
.oe-title-abstract-label {
text-align: center;
font-weight: bold;
margin-bottom: 6px;
font-size: 0.9em;
line-height: 1.3em;
}
.oe-title-abstract-content {
width: 80%;
margin: auto;
.CodeMirror {
font-size: 0.9em;
line-height: 1.3em;
}
}
}
}
.oe-widget {
border: 1px solid transparent;
margin: 6px 0;
}
.oe-widget-selected {
border: 1px dashed #bbb;
.oe-figure-toolbar {
display: block;
}
.ui-wrapper {
border: 1px dashed gray;
}
}
.oe-block-math {
.MathJax_Display {
margin: 12px 0;
}
.oe-block-math-toolbar {
text-align: right;
padding: 4px;
z-index: 100;
.bootstrap-select.btn-group {
margin: 0;
}
}
.CodeMirror {
font-family: "Computer Modern Monospace", monospace;
text-align: center;
}
}
.oe-non-editable {
color: gray;
cursor: default;
}
.oe-bibliography {
padding: 4px;
cursor: pointer;
}
}
// These are needed by the citation drop down too, so
// don't wrap them in .oe-bibliography
.oe-bibliography-entry {
display: inline-block;
}
.oe-bibliography-year {
font-weight: bold;
}
.oe-bibliography-author {
font-weight: bold;
}
.oe-bibliography-title {
display: block;
margin-left: 10px;
}
.oe-bibliography {
.oe-bibliography-header {
font-weight: bold;
font-size: 1.8em !important;
line-height: 2em !important;
}
.oe-bibliography-entry-wrapper {
margin-top: 6px;
}
.CodeMirror {
font-family: "Computer Modern Monospace", monospace;
color: green;
height: 400px;
}
.oe-bibliography-no-entries {
font-style: italic;
}
.oe-bibliography-error {
color: red;
}
}
.bootstrap-select.oe-inline-select {
margin: 0;
vertical-align: bottom;
> .btn {
border: none;
background: none;
box-shadow: none;
padding: 0;
margin: 0;
.editor-font;
color: rgb(2, 151, 172);
}
}

View file

@ -0,0 +1,116 @@
@changesListWidth: 250px;
@changesListPadding: 10px;
#trackChangesPanel {
.track-changes-diff {
position: absolute;
right: @changesListWidth + 1px;
left: 0;
top: 0;
bottom: 0;
padding: 0px 12px;
height: 100%;
.ace_editor {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.track-changes-side-bar {
border-left: 1px solid #999;
height: 100%;
width: @changesListWidth;
position: absolute;
right: 0;
background-color: white;
.track-changes-header {
background-color: black;
h3 {
color: white;
padding-left: 8px;
font-size: 1.2em;
}
a {
color: white;
position: absolute;
top: 6px;
right: 8px;
}
height: 30px;
}
.change-list-area {
overflow: scroll;
position: absolute;
left: 0px;
right: 0px;
top: 30px;
bottom: 0px;
}
}
.deleted-change-background, .deleted-change-foreground, .inserted-change, .change-name-marker {
position: absolute;
z-index: 2;
}
.change-name-marker {
.name {
font-size: 0.8em;
padding: 2px 6px;
.border-radius(3px 3px 3px 3px);
position: absolute;
border: 1px solid #999;
left: 0;
}
}
ul.change-list {
li {
padding: 6px 4px;
position: relative;
border-bottom: 1px solid #ccc;
cursor: pointer;
.change-selectors {
position: absolute;
top: 4px;
left: 6px;
.change-selector-from {
position: absolute;
top: 0;
left: 0;
}
.change-selector-to {
position: absolute;
top: 0;
left: 20px;
}
}
.change-description {
padding-left: 42px;
}
.change-name {
font-size: 0.9em;
color: #666;
text-transform: capitalize;
}
.color-square {
display: inline-block;
height: 10px;
width: 10px;
margin-right: 4px;
margin-bottom: -1px;
}
&:hover {
background-color: #eaeaea;
}
}
li.selected {
background-color: #eaeaea;
}
}
}

View file

@ -49,6 +49,7 @@
@import "public/stylesheets/less/navbar.less";
@import "public/stylesheets/less/footer.less";
@import "public/stylesheets/less/list.less";
@import "public/stylesheets/less/trackchanges.less";
@import "public/stylesheets/less/fileuploader.less";

View file

@ -0,0 +1,46 @@
chai = require('chai')
chai.should()
sinon = require("sinon")
modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesController"
SandboxedModule = require('sandboxed-module')
describe "TrackChangesController", ->
beforeEach ->
@TrackChangesController = SandboxedModule.require modulePath, requires:
"request" : @request = {}
"settings-sharelatex": @settings = {}
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
describe "proxyToTrackChangesApi", ->
beforeEach ->
@req = { url: "/mock/url" }
@res = "mock-res"
@next = sinon.stub()
@settings.apis =
trackchanges:
url: "http://trackchanges.example.com"
@proxy =
events: {}
pipe: sinon.stub()
on: (event, handler) -> @events[event] = handler
@request.get = sinon.stub().returns @proxy
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
describe "successfully", ->
it "should call the track changes api", ->
@request.get
.calledWith("#{@settings.apis.trackchanges.url}#{@req.url}")
.should.equal true
it "should pipe the response to the client", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
describe "with an error", ->
beforeEach ->
@proxy.events["error"].call(@proxy, @error = new Error("oops"))
it "should pass the error up the call chain", ->
@next.calledWith(@error).should.equal true