Merge branch 'master' into ho-annom-user-events

This commit is contained in:
Henry Oswald
2017-03-27 09:28:03 +01:00
committed by GitHub
242 changed files with 60938 additions and 157492 deletions

View File

@@ -1,9 +1,15 @@
AnalyticsManager = require "./AnalyticsManager"
AuthenticationController = require("../Authentication/AuthenticationController")
Errors = require "../Errors/Errors"
module.exports = AnalyticsController =
recordEvent: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req) or req.sessionID
AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) ->
return next(error) if error?
if error?
if error instanceof Errors.ServiceNotConfiguredError
# ignore, no-op
return res.send(204)
else
return next(error)
res.send 204

View File

@@ -2,6 +2,7 @@ settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
_ = require "underscore"
request = require "request"
Errors = require '../Errors/Errors'
makeRequest = (opts, callback)->
@@ -10,8 +11,7 @@ makeRequest = (opts, callback)->
opts.url = "#{settings.apis.analytics.url}#{urlPath}"
request opts, callback
else
callback()
callback(new Errors.ServiceNotConfiguredError('Analytics service not configured'))
module.exports =

View File

@@ -10,7 +10,7 @@ module.exports = BlogHandler =
opts =
url:blogUrl
json:true
timeout:500
timeout:1000
request.get opts, (err, res, announcements)->
if err?
return callback err
@@ -18,7 +18,6 @@ module.exports = BlogHandler =
return callback("blog announcement returned non 200")
logger.log announcementsLength: announcements?.length, "announcements returned"
announcements = _.map announcements, (announcement)->
announcement.url = "/blog#{announcement.url}"
announcement.date = new Date(announcement.date)
return announcement
callback(err, announcements)

View File

@@ -4,9 +4,9 @@ logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController')
CommentsController = require('../Comments/CommentsController')
async = require "async"
module.exports =
module.exports = ChatController =
sendMessage: (req, res, next)->
project_id = req.params.project_id
content = req.body.content
@@ -28,7 +28,38 @@ module.exports =
logger.log project_id:project_id, query:query, "getting messages"
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
return next(err) if err?
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
ChatController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
return next(err) if err?
logger.log length: messages?.length, "sending messages to client"
res.json messages
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
userCache = {}
getUserDetails = (user_id, callback = (error, user) ->) ->
return callback(null, userCache[user_id]) if userCache[user_id]?
UserInfoManager.getPersonalInfo user_id, (err, user) ->
return callback(error) if error?
user = UserInfoController.formatPersonalInfo user
userCache[user_id] = user
callback null, user
jobs = []
for thread_id, thread of threads
do (thread) ->
if thread.resolved
jobs.push (cb) ->
getUserDetails thread.resolved_by_user_id, (error, user) ->
cb(error) if error?
thread.resolved_by_user = user
cb()
for message in thread.messages
do (message) ->
jobs.push (cb) ->
getUserDetails message.user_id, (error, user) ->
cb(error) if error?
message.user = user
cb()
async.series jobs, (error) ->
return callback(error) if error?
return callback null, threads

View File

@@ -1,111 +0,0 @@
ChatApiHandler = require("../Chat/ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController')
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
async = require "async"
module.exports = CommentsController =
sendComment: (req, res, next) ->
{project_id, thread_id} = req.params
content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id?
err = new Error('no logged-in user')
return next(err)
logger.log {project_id, thread_id, user_id, content}, "sending comment"
ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) ->
return next(err) if err?
UserInfoManager.getPersonalInfo comment.user_id, (err, user) ->
return next(err) if err?
comment.user = UserInfoController.formatPersonalInfo(user)
EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) ->
res.send 204
getThreads: (req, res, next) ->
{project_id} = req.params
logger.log {project_id}, "getting comment threads for project"
ChatApiHandler.getThreads project_id, (err, threads) ->
return next(err) if err?
CommentsController._injectUserInfoIntoThreads threads, (error, threads) ->
return next(err) if err?
res.json threads
resolveThread: (req, res, next) ->
{project_id, thread_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, thread_id, user_id}, "resolving comment thread"
ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) ->
return next(err) if err?
UserInfoManager.getPersonalInfo user_id, (err, user) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)->
res.send 204
reopenThread: (req, res, next) ->
{project_id, thread_id} = req.params
logger.log {project_id, thread_id}, "reopening comment thread"
ChatApiHandler.reopenThread project_id, thread_id, (err, threads) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
res.send 204
deleteThread: (req, res, next) ->
{project_id, doc_id, thread_id} = req.params
logger.log {project_id, doc_id, thread_id}, "deleting comment thread"
DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) ->
return next(err) if err?
ChatApiHandler.deleteThread project_id, thread_id, (err, threads) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)->
res.send 204
editMessage: (req, res, next) ->
{project_id, thread_id, message_id} = req.params
{content} = req.body
logger.log {project_id, thread_id, message_id}, "editing message thread"
ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)->
res.send 204
deleteMessage: (req, res, next) ->
{project_id, thread_id, message_id} = req.params
logger.log {project_id, thread_id, message_id}, "deleting message"
ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_id, (err)->
res.send 204
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
userCache = {}
getUserDetails = (user_id, callback = (error, user) ->) ->
return callback(null, userCache[user_id]) if userCache[user_id]?
UserInfoManager.getPersonalInfo user_id, (err, user) ->
return callback(error) if error?
user = UserInfoController.formatPersonalInfo user
userCache[user_id] = user
callback null, user
jobs = []
for thread_id, thread of threads
do (thread) ->
if thread.resolved
jobs.push (cb) ->
getUserDetails thread.resolved_by_user_id, (error, user) ->
cb(error) if error?
thread.resolved_by_user = user
cb()
for message in thread.messages
do (message) ->
jobs.push (cb) ->
getUserDetails message.user_id, (error, user) ->
cb(error) if error?
message.user = user
cb()
async.series jobs, (error) ->
return callback(error) if error?
return callback null, threads

View File

@@ -5,5 +5,14 @@ NotFoundError = (message) ->
return error
NotFoundError.prototype.__proto__ = Error.prototype
ServiceNotConfiguredError = (message) ->
error = new Error(message)
error.name = "ServiceNotConfiguredError"
error.__proto__ = ServiceNotConfiguredError.prototype
return error
module.exports = Errors =
NotFoundError: NotFoundError
NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError

View File

@@ -25,10 +25,19 @@ module.exports = FileStoreHandler =
timeout:fiveMinsInMs
writeStream = request(opts)
readStream.pipe writeStream
writeStream.on "end", callback
writeStream.on 'response', (response) ->
if response.statusCode not in [200, 201]
err = new Error("non-ok response from filestore for upload: #{response.statusCode}")
logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
callback(err)
else
callback(null)
readStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
callback err
writeStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
callback err
@@ -79,4 +88,4 @@ module.exports = FileStoreHandler =
callback(err)
_buildUrl: (project_id, file_id)->
return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"
return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"

View File

@@ -224,6 +224,11 @@ module.exports = ProjectController =
cb = underscore.once(cb)
if !user_id?
return cb()
timestamp = user_id.toString().substring(0,8)
userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 )
if userSignupDate > new Date("2017-03-09") # 8th March
# Don't show for users who registered after it was released
return cb(null, false)
timeout = setTimeout cb, 500
AnalyticsManager.getLastOccurance user_id, "shown-track-changes-onboarding-2", (error, event) ->
clearTimeout timeout

View File

@@ -1,6 +1,8 @@
_ = require("underscore")
module.exports = ProjectEditorHandler =
trackChangesAvailable: false
buildProjectModelView: (project, members, invites) ->
result =
_id : project._id
@@ -20,11 +22,6 @@ module.exports = ProjectEditorHandler =
if !result.invites?
result.invites = []
trackChangesVisible = false
for member in members
if member.privilegeLevel == "owner" and (member.user?.featureSwitches?.track_changes or member.user?.betaProgram)
trackChangesVisible = true
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner
result.members = members
@@ -38,7 +35,7 @@ module.exports = ProjectEditorHandler =
templates: false
references: false
trackChanges: false
trackChangesVisible: trackChangesVisible
trackChangesVisible: ProjectEditorHandler.trackChangesAvailable
})
return result

View File

@@ -16,6 +16,8 @@ module.exports =
webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide")
webRouter.get '/jobs', HomeController.externalPage("jobs", "Jobs")
webRouter.get '/track-changes-and-comments-in-latex', HomeController.externalPage("review-features-page", "Review features")
webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")
webRouter.get '/university', UniversityController.getIndexPage

View File

@@ -23,7 +23,7 @@ module.exports = SystemMessageManager =
clearCache: () ->
delete @_cachedMessages
CACHE_TIMEOUT = 5 * 60 * 1000 # 5 minutes
CACHE_TIMEOUT = 20 * 1000 # 20 seconds
setInterval () ->
SystemMessageManager.clearCache()
, CACHE_TIMEOUT
, CACHE_TIMEOUT

View File

@@ -1,23 +0,0 @@
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
DocstoreManager = require "../Docstore/DocstoreManager"
UserInfoManager = require "../User/UserInfoManager"
async = require "async"
module.exports = RangesManager =
getAllRanges: (project_id, callback = (error, docs) ->) ->
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
DocstoreManager.getAllRanges project_id, callback
getAllChangesUsers: (project_id, callback = (error, users) ->) ->
user_ids = {}
RangesManager.getAllRanges project_id, (error, docs) ->
return callback(error) if error?
jobs = []
for doc in docs
for change in doc.ranges?.changes or []
user_ids[change.metadata.user_id] = true
async.mapSeries Object.keys(user_ids), (user_id, cb) ->
UserInfoManager.getPersonalInfo user_id, cb
, callback

View File

@@ -1,42 +0,0 @@
RangesManager = require "./RangesManager"
logger = require "logger-sharelatex"
UserInfoController = require "../User/UserInfoController"
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
TrackChangesManager = require "./TrackChangesManager"
module.exports = TrackChangesController =
getAllRanges: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "request for project ranges"
RangesManager.getAllRanges project_id, (error, docs = []) ->
return next(error) if error?
docs = ({id: d._id, ranges: d.ranges} for d in docs)
res.json docs
getAllChangesUsers: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "request for project range users"
RangesManager.getAllChangesUsers project_id, (error, users) ->
return next(error) if error?
users = (UserInfoController.formatPersonalInfo(user) for user in users)
# Get rid of any anonymous/deleted user objects
users = users.filter (u) -> u?.id?
res.json users
acceptChange: (req, res, next) ->
{project_id, doc_id, change_id} = req.params
logger.log {project_id, doc_id, change_id}, "request to accept change"
DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)->
res.send 204
toggleTrackChanges: (req, res, next) ->
{project_id} = req.params
track_changes_on = !!req.body.on
logger.log {project_id, track_changes_on}, "request to toggle track changes"
TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)->
res.send 204

View File

@@ -1,5 +0,0 @@
Project = require("../../models/Project").Project
module.exports = TrackChangesManager =
toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
Project.update {_id: project_id}, {track_changes: track_changes_on}, callback

View File

@@ -1,5 +1,5 @@
version = {
"pdfjs": "1.6.210p2"
"pdfjs": "1.7.225"
"moment": "2.9.0"
"ace": "1.2.5"
}

View File

@@ -39,9 +39,6 @@ UserSchema = new Schema
references: { type:Boolean, default: Settings.defaultFeatures.references }
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
}
featureSwitches : {
track_changes: { type: Boolean }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]
refered_user_count: { type:Number, default: 0 }

View File

@@ -40,8 +40,6 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
CommentsController = require "./Features/Comments/CommentsController"
logger = require("logger-sharelatex")
_ = require("underscore")
@@ -177,11 +175,6 @@ module.exports = class Router
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges
webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers
webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange
webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@@ -232,15 +225,6 @@ module.exports = class Router
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
# Note: Read only users can still comment
webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
webRouter.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread
webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage
webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll

View File

@@ -18,9 +18,7 @@ block content
| #{translate("beta_program_badge_description")}
span.beta-feature-badge
p.text-centered
strong We're currently testing track changes and commenting:
p.text-centered
img(src="/img/teasers/track-changes/track-changes-beta.png", style="max-width: 100%; border-bottom: 1px solid #ddd")
strong We're not currently testing anything in beta, but keep checking back!
.row.text-centered
.col-md-12
if user.betaProgram

View File

@@ -19,7 +19,8 @@ div.full-size(
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
'rp-size-expanded': ui.reviewPanelOpen,\
'rp-layout-left': reviewPanel.layoutToLeft,\
'rp-loading-threads': reviewPanel.loadingThreads\
'rp-loading-threads': reviewPanel.loadingThreads,\
'rp-new-comment-ui': reviewPanel.newAddCommentUI\
}"
)
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")

View File

@@ -94,7 +94,6 @@ header.toolbar.toolbar-header.toolbar-with-labels(
i.review-icon
p.toolbar-label
| #{translate("review")}
span(style="vertical-align: 20%; margin-left: 4px; padding: 2px 4px;").beta-feature-badge
a.btn.btn-full-height(
href,
ng-if="permissions.admin",

View File

@@ -1,10 +1,18 @@
#review-panel
.rp-in-editor-widgets
a.rp-track-changes-indicator(
href
ng-if="editor.wantTrackChanges"
ng-click="toggleReviewPanel();"
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
) !{translate("track_changes_is_on")}
a.rp-add-comment-btn(
href
ng-if="reviewPanel.newAddCommentUI && reviewPanel.entries[editor.open_doc_id]['add-comment'] != null"
ng-click="addNewComment();"
)
i.fa.fa-comment
|  #{translate("add_comment")}
#review-panel
.review-panel-toolbar
resolved-comments-dropdown(
class="rp-flex-block"
@@ -314,7 +322,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate')
script(type='text/ng-template', id='addCommentEntryTemplate')
div
.rp-entry-callout.rp-entry-callout-add-comment
.rp-entry-indicator(
.rp-entry-indicator.rp-entry-indicator-add-comment(
ng-if="!commentState.adding"
ng-click="startNewComment(); onIndicatorClick();"
tooltip=translate("add_comment")
@@ -340,10 +348,10 @@ script(type='text/ng-template', id='addCommentEntryTemplate')
ng-keypress="handleCommentKeyPress($event);"
placeholder=translate("add_your_comment_here")
focus-on="comment:new:open"
ng-blur="submitNewComment()"
ng-blur="submitNewComment($event)"
)
.rp-entry-actions
button.rp-entry-button(
button.rp-entry-button.rp-entry-button-cancel(
ng-click="cancelNewComment();"
)
i.fa.fa-times

View File

@@ -4,11 +4,11 @@ block scripts
script(src="https://js.recurly.com/v3/recurly.js")
script(type='text/javascript').
window.recomendedCurrency = '#{currency}'
window.countryCode = '#{countryCode}'
window.plan_code = '#{plan_code}'
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
window.couponCode = "#{couponCode}"
window.couponCode = !{JSON.stringify(couponCode)}
window.recomendedCurrency = !{JSON.stringify(currency.slice(0,3))}
block content
.content.content-alt

View File

@@ -353,11 +353,17 @@ define [
@ranges.applyOp op, { user_id: track_changes_as }
if old_id_seed?
@ranges.setIdSeed(old_id_seed)
if remote_op
# With remote ops, Ace hasn't been updated when we receive this op,
# so defer updating track changes until it has
setTimeout () => @emit "ranges:dirty"
else
@emit "ranges:dirty"
_catchUpRanges: (changes = [], comments = []) ->
# We've just been given the current server's ranges, but need to apply any local ops we have.
# Reset to the server state then apply our local ops again.
@ranges.emit "clear"
@emit "ranges:clear"
@ranges.changes = changes
@ranges.comments = comments
@ranges.track_changes = @doc.track_changes
@@ -367,4 +373,4 @@ define [
for op in @doc.getPendingOp() or []
@ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
@ranges.applyOp(op, { user_id: @track_changes_as })
@ranges.emit "redraw"
@emit "ranges:redraw"

View File

@@ -121,11 +121,22 @@ define [
_bindToDocumentEvents: (doc, sharejs_doc) ->
sharejs_doc.on "error", (error, meta) =>
if error?.message?.match "maxDocLength"
if error?.message?
message = error.message
else if typeof error == "string"
message = error
else
message = ""
if message.match "maxDocLength"
@ide.showGenericMessageModal(
"Document Too Long"
"Sorry, this file is too long to be edited manually. Please upload it directly."
)
else if message.match "too many comments or tracked changes"
@ide.showGenericMessageModal(
"Too many comments or tracked changes"
"Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments."
)
else
@ide.socket.disconnect()
@ide.reportError(error, meta)

View File

@@ -322,10 +322,6 @@ define [
doc = session.getDocument()
doc.on "change", onChange
sharejs_doc.on "remoteop.recordRemote", (op, oldSnapshot, msg) ->
undoManager.nextUpdateIsRemote = true
trackChangesManager.nextUpdateMetaData = msg?.meta
editor.initing = true
sharejs_doc.attachToAce(editor)
editor.initing = false

View File

@@ -14,11 +14,11 @@ define [
return if !track_changes?
@setTrackChanges(track_changes)
@$scope.$watch "sharejsDoc", (doc) =>
@$scope.$watch "sharejsDoc", (doc, oldDoc) =>
return if !doc?
@disconnectFromRangesTracker()
@rangesTracker = doc.ranges
@connectToRangesTracker()
if oldDoc?
@disconnectFromDoc(oldDoc)
@connectToDoc(doc)
@$scope.$on "comment:add", (e, thread_id, offset, length) =>
@addCommentToSelection(thread_id, offset, length)
@@ -36,10 +36,10 @@ define [
@removeCommentId(comment_id)
@$scope.$on "comment:resolve_threads", (e, thread_ids) =>
@resolveCommentByThreadIds(thread_ids)
@hideCommentsByThreadIds(thread_ids)
@$scope.$on "comment:unresolve_thread", (e, thread_id) =>
@unresolveCommentByThreadId(thread_id)
@showCommentByThreadId(thread_id)
@$scope.$on "review-panel:recalculate-screen-positions", () =>
@recalculateReviewEntriesScreenPositions()
@@ -73,16 +73,24 @@ define [
_scrollTimeout = null
, 200
@_resetCutState()
onCut = () => @onCut()
onPaste = () => @onPaste()
bindToAce = () =>
@editor.on "changeSelection", onChangeSelection
@editor.on "change", onChangeSelection # Selection also moves with updates elsewhere in the document
@editor.on "changeSession", onChangeSession
@editor.on "cut", onCut
@editor.on "paste", onPaste
@editor.renderer.on "resize", onResize
unbindFromAce = () =>
@editor.off "changeSelection", onChangeSelection
@editor.off "change", onChangeSelection
@editor.off "changeSession", onChangeSession
@editor.off "cut", onCut
@editor.off "paste", onPaste
@editor.renderer.off "resize", onResize
@$scope.$watch "trackChangesEnabled", (enabled) =>
@@ -92,18 +100,11 @@ define [
else
unbindFromAce()
disconnectFromRangesTracker: () ->
disconnectFromDoc: (doc) ->
@changeIdToMarkerIdMap = {}
if @rangesTracker?
@rangesTracker.off "insert:added"
@rangesTracker.off "insert:removed"
@rangesTracker.off "delete:added"
@rangesTracker.off "delete:removed"
@rangesTracker.off "changes:moved"
@rangesTracker.off "comment:added"
@rangesTracker.off "comment:moved"
@rangesTracker.off "comment:removed"
doc.off "ranges:clear"
doc.off "ranges:redraw"
doc.off "ranges:dirty"
setTrackChanges: (value) ->
if value
@@ -111,56 +112,15 @@ define [
else
@$scope.sharejsDoc?.track_changes_as = null
connectToRangesTracker: () ->
connectToDoc: (doc) ->
@rangesTracker = doc.ranges
@setTrackChanges(@$scope.trackChanges)
# Add a timeout because on remote ops, we get these notifications before
# ace has updated
@rangesTracker.on "insert:added", (change) =>
sl_console.log "[insert:added]", change
setTimeout () =>
@_onInsertAdded(change)
@broadcastChange()
@rangesTracker.on "insert:removed", (change) =>
sl_console.log "[insert:removed]", change
setTimeout () =>
@_onInsertRemoved(change)
@broadcastChange()
@rangesTracker.on "delete:added", (change) =>
sl_console.log "[delete:added]", change
setTimeout () =>
@_onDeleteAdded(change)
@broadcastChange()
@rangesTracker.on "delete:removed", (change) =>
sl_console.log "[delete:removed]", change
setTimeout () =>
@_onDeleteRemoved(change)
@broadcastChange()
@rangesTracker.on "changes:moved", (changes) =>
sl_console.log "[changes:moved]", changes
setTimeout () =>
@_onChangesMoved(changes)
@broadcastChange()
@rangesTracker.on "comment:added", (comment) =>
sl_console.log "[comment:added]", comment
setTimeout () =>
@_onCommentAdded(comment)
@broadcastChange()
@rangesTracker.on "comment:moved", (comment) =>
sl_console.log "[comment:moved]", comment
setTimeout () =>
@_onCommentMoved(comment)
@broadcastChange()
@rangesTracker.on "comment:removed", (comment) =>
sl_console.log "[comment:removed]", comment
setTimeout () =>
@_onCommentRemoved(comment)
@broadcastChange()
@rangesTracker.on "clear", () =>
doc.on "ranges:dirty", () =>
@updateAnnotations()
doc.on "ranges:clear", () =>
@clearAnnotations()
@rangesTracker.on "redraw", () =>
doc.on "ranges:redraw", () =>
@redrawAnnotations()
clearAnnotations: () ->
@@ -181,6 +141,55 @@ define [
@_onCommentAdded(comment)
@broadcastChange()
_doneUpdateThisLoop: false
_pendingUpdates: false
updateAnnotations: () ->
# Doc updates with multiple ops, like search/replace or block comments
# will call this with every individual op in a single event loop. So only
# do the first this loop, then schedule an update for the next loop for the rest.
if !@_doneUpdateThisLoop
@_doUpdateAnnotations()
@_doneUpdateThisLoop = true
setTimeout () =>
if @_pendingUpdates
@_doUpdateAnnotations()
@_doneUpdateThisLoop = false
@_pendingUpdates = false
else
@_pendingUpdates = true
_doUpdateAnnotations: () ->
dirty = @rangesTracker.getDirtyState()
updateMarkers = false
for id, change of dirty.change.added
if change.op.i?
@_onInsertAdded(change)
else if change.op.d?
@_onDeleteAdded(change)
for id, change of dirty.change.removed
if change.op.i?
@_onInsertRemoved(change)
else if change.op.d?
@_onDeleteRemoved(change)
for id, change of dirty.change.moved
updateMarkers = true
@_onChangeMoved(change)
for id, comment of dirty.comment.added
@_onCommentAdded(comment)
for id, comment of dirty.comment.removed
@_onCommentRemoved(comment)
for id, comment of dirty.comment.moved
updateMarkers = true
@_onCommentMoved(comment)
@rangesTracker.resetDirtyState()
if updateMarkers
@editor.renderer.updateBackMarkers()
@broadcastChange()
addComment: (offset, content, thread_id) ->
op = { c: content, p: offset, t: thread_id }
@@ -200,6 +209,7 @@ define [
acceptChangeId: (change_id) ->
@rangesTracker.removeChangeId(change_id)
@updateAnnotations()
rejectChangeId: (change_id) ->
change = @rangesTracker.getChange(change_id)
@@ -208,21 +218,26 @@ define [
if change.op.d?
content = change.op.d
position = @_shareJsOffsetToAcePosition(change.op.p)
session.$fromReject = true # Tell track changes to cancel out delete
session.insert(position, content)
session.$fromReject = false
else if change.op.i?
start = @_shareJsOffsetToAcePosition(change.op.p)
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
editor_text = session.getDocument().getTextRange({start, end})
if editor_text != change.op.i
throw new Error("Op to be removed (#{JSON.stringify(change.op)}), does not match editor text, '#{editor_text}'")
session.$fromReject = true
session.remove({start, end})
session.$fromReject = false
else
throw new Error("unknown change: #{JSON.stringify(change)}")
removeCommentId: (comment_id) ->
@rangesTracker.removeCommentId(comment_id)
@updateAnnotations()
resolveCommentByThreadIds: (thread_ids) ->
hideCommentsByThreadIds: (thread_ids) ->
resolve_ids = {}
for id in thread_ids
resolve_ids[id] = true
@@ -231,12 +246,55 @@ define [
@_onCommentRemoved(comment)
@broadcastChange()
unresolveCommentByThreadId: (thread_id) ->
showCommentByThreadId: (thread_id) ->
for comment in @rangesTracker?.comments or []
if comment.op.t == thread_id
@_onCommentAdded(comment)
@broadcastChange()
_resetCutState: () ->
@_cutState = {
text: null
comments: []
docId: null
}
onCut: () ->
@_resetCutState()
selection = @editor.getSelectionRange()
selection_start = @_aceRangeToShareJs(selection.start)
selection_end = @_aceRangeToShareJs(selection.end)
@_cutState.text = @editor.getSelectedText()
@_cutState.docId = @$scope.docId
for comment in @rangesTracker.comments
comment_start = comment.op.p
comment_end = comment_start + comment.op.c.length
if selection_start <= comment_start and comment_end <= selection_end
@_cutState.comments.push {
offset: comment.op.p - selection_start
text: comment.op.c
comment: comment
}
onPaste: () =>
@editor.once "change", (change) =>
return if change.action != "insert"
pasted_text = change.lines.join("\n")
paste_offset = @_aceRangeToShareJs(change.start)
# We have to wait until the change has been processed by the range tracker,
# since if we move the ops into place beforehand, they will be moved again
# when the changes are processed by the range tracker. This ranges:dirty
# event is fired after the doc has applied the changes to the range tracker.
@$scope.sharejsDoc.on "ranges:dirty.paste", () =>
@$scope.sharejsDoc.off "ranges:dirty.paste" # Doc event emitter uses namespaced events
if pasted_text == @_cutState.text and @$scope.docId == @_cutState.docId
for {comment, offset, text} in @_cutState.comments
op = { c: text, p: paste_offset + offset, t: comment.id }
@$scope.sharejsDoc.submitOp op # Resubmitting an existing comment op (by thread id) will move it
@_resetCutState()
# Check that comments still match text. Will throw error if not.
@rangesTracker.validate(@editor.getValue())
checkMapping: () ->
# TODO: reintroduce this check
session = @editor.getSession()
@@ -421,23 +479,18 @@ define [
lines = @editor.getSession().getDocument().getAllLines()
return AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
_onChangesMoved: (changes) ->
# TODO: PERFORMANCE: Only run through the Ace lines once, and calculate all
# change positions as we go.
for change in changes
start = @_shareJsOffsetToAcePosition(change.op.p)
if change.op.i?
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
else
end = start
@_updateMarker(change.id, start, end)
@editor.renderer.updateBackMarkers()
_onChangeMoved: (change) ->
start = @_shareJsOffsetToAcePosition(change.op.p)
if change.op.i?
end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
else
end = start
@_updateMarker(change.id, start, end)
_onCommentMoved: (comment) ->
start = @_shareJsOffsetToAcePosition(comment.op.p)
end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
@_updateMarker(comment.id, start, end)
@editor.renderer.updateBackMarkers()
_updateMarker: (change_id, start, end) ->
return if !@changeIdToMarkerIdMap[change_id]?

View File

@@ -11,10 +11,10 @@ define [
show_remote_warning: false
@reset()
@nextUpdateIsRemote = false
@editor.on "changeSession", (e) =>
@reset()
@session = e.session
e.session.setUndoManager(@)
showUndoConflictWarning: () ->
@@ -38,20 +38,44 @@ define [
@firstUpdate = false
return
aceDeltaSets = options.args[0]
@session = options.args[1]
return if !aceDeltaSets?
@session = options.args[1]
lines = @session.getDocument().getAllLines()
linesBeforeChange = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, lines)
simpleDeltaSets = @_aceDeltaSetsToSimpleDeltaSets(aceDeltaSets, linesBeforeChange)
@undoStack.push(
deltaSets: simpleDeltaSets
remote: @nextUpdateIsRemote
)
# We need to split the delta sets into local or remote groups before pushing onto
# the undo stack, since these are treated differently.
splitDeltaSets = []
currentDeltaSet = null # Make global to this function
do newDeltaSet = () ->
currentDeltaSet = {group: "doc", deltas: []}
splitDeltaSets.push currentDeltaSet
currentRemoteState = null
for deltaSet in aceDeltaSets or []
if deltaSet.group == "doc" # ignore code folding etc.
for delta in deltaSet.deltas
if currentDeltaSet.remote? and currentDeltaSet.remote != !!delta.remote
newDeltaSet()
currentDeltaSet.deltas.push delta
currentDeltaSet.remote = !!delta.remote
# The lines are currently as they are after applying all these deltas, but to turn into simple deltas,
# we need the lines before each delta group.
docLines = @session.getDocument().getAllLines()
docLines = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, docLines)
for deltaSet in splitDeltaSets
{simpleDeltaSet, docLines} = @_aceDeltaSetToSimpleDeltaSet(deltaSet, docLines)
frame = {
deltaSets: [simpleDeltaSet]
remote: deltaSet.remote
}
@undoStack.push frame
@redoStack = []
@nextUpdateIsRemote = false
undo: (dontSelect) ->
# We rely on the doclines being in sync with the undo stack, so make sure
# any pending undo deltas are processed.
@session.$syncInformUndoManager()
localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
return if !localUpdatesMade
@@ -206,19 +230,16 @@ define [
throw "Unknown delta type"
return doc.split("\n")
_aceDeltaSetsToSimpleDeltaSets: (aceDeltaSets, docLines) ->
simpleDeltaSets = []
for deltaSet in aceDeltaSets
if deltaSet.group == "doc" # ignore fold changes
simpleDeltas = []
for delta in deltaSet.deltas
simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
docLines = @_applyAceDeltasToDocLines([delta], docLines)
simpleDeltaSets.push {
deltas: simpleDeltas
group: deltaSet.group
}
return simpleDeltaSets
_aceDeltaSetToSimpleDeltaSet: (deltaSet, docLines) ->
simpleDeltas = []
for delta in deltaSet.deltas
simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
docLines = @_applyAceDeltasToDocLines([delta], docLines)
simpleDeltaSet = {
deltas: simpleDeltas
group: deltaSet.group
}
return {simpleDeltaSet, docLines}
_simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
for deltaSet in simpleDeltaSets

View File

@@ -3,7 +3,7 @@
Range = ace.require("ace/range").Range
# Convert an ace delta into an op understood by share.js
applyToShareJS = (editorDoc, delta, doc) ->
applyToShareJS = (editorDoc, delta, doc, fromUndo) ->
# Get the start position of the range, in no. of characters
getStartOffsetPosition = (start) ->
# This is quite inefficient - getLines makes a copy of the entire
@@ -27,11 +27,11 @@ applyToShareJS = (editorDoc, delta, doc) ->
switch delta.action
when 'insert'
text = delta.lines.join('\n')
doc.insert pos, text
doc.insert pos, text, fromUndo
when 'remove'
text = delta.lines.join('\n')
doc.del pos, text.length
doc.del pos, text.length, fromUndo
else throw new Error "unknown action: #{delta.action}"
@@ -78,8 +78,10 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
if maxDocLength? and editorDoc.getValue().length > maxDocLength
doc.emit "error", new Error("document length is greater than maxDocLength")
return
fromUndo = !!(editor.getSession().$fromUndo or editor.getSession().$fromReject)
applyToShareJS editorDoc, change, doc
applyToShareJS editorDoc, change, doc, fromUndo
check()
@@ -108,16 +110,46 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
row:row, column:offset
# We want to insert a remote:true into the delta if the op comes from the
# underlying sharejs doc (which means it is from a remote op), so we have to do
# the work of editorDoc.insert and editorDoc.remove manually. These methods are
# copied from ace.js doc#insert and #remove, and then inject the remote:true
# flag into the delta.
doc.on 'insert', (pos, text) ->
if (editorDoc.getLength() <= 1)
editorDoc.$detectNewLine(text)
lines = editorDoc.$split(text)
position = offsetToPos(pos)
start = editorDoc.clippedPos(position.row, position.column)
end = {
row: start.row + lines.length - 1,
column: (if lines.length == 1 then start.column else 0) + lines[lines.length - 1].length
}
suppress = true
editorDoc.insert offsetToPos(pos), text
editorDoc.applyDelta({
start: start,
end: end,
action: "insert",
lines: lines,
remote: true
});
suppress = false
check()
doc.on 'delete', (pos, text) ->
suppress = true
range = Range.fromPoints offsetToPos(pos), offsetToPos(pos + text.length)
editorDoc.remove range
start = editorDoc.clippedPos(range.start.row, range.start.column)
end = editorDoc.clippedPos(range.end.row, range.end.column)
suppress = true
editorDoc.applyDelta({
start: start,
end: end,
action: "remove",
lines: editorDoc.getLinesForRange({start: start, end: end})
remote: true
});
suppress = false
check()

View File

@@ -11,14 +11,20 @@ text.api =
# Get the text contents of a document
getText: -> @snapshot
insert: (pos, text, callback) ->
op = [{p:pos, i:text}]
insert: (pos, text, fromUndo, callback) ->
op = {p:pos, i:text}
if fromUndo
op.u = true
op = [op]
@submitOp op, callback
op
del: (pos, length, callback) ->
op = [{p:pos, d:@snapshot[pos...(pos + length)]}]
del: (pos, length, fromUndo, callback) ->
op = {p:pos, d:@snapshot[pos...(pos + length)]}
if fromUndo
op.u = true
op = [op]
@submitOp op, callback
op

View File

@@ -56,6 +56,13 @@ text.apply = (snapshot, op) ->
throw new Error "Unknown op type"
snapshot
cloneAndModify = (op, modifications) ->
newOp = {}
for k,v of op
newOp[k] = v
for k,v of modifications
newOp[k] = v
return newOp
# Exported for use by the random op generator.
#
@@ -69,10 +76,10 @@ text._append = append = (newOp, c) ->
last = newOp[newOp.length - 1]
# Compose the insert into the previous insert if possible
if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length)
newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}
else if last.d? && c.d? and c.p <= last.p <= (c.p + c.d.length)
newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}
if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length) and last.u == c.u
newOp[newOp.length - 1] = cloneAndModify(last, {i:strInject(last.i, c.p - last.p, c.i)})
else if last.d? && c.d? and c.p <= last.p <= (c.p + c.d.length) and last.u == c.u
newOp[newOp.length - 1] = cloneAndModify(last, {d:strInject(c.d, last.p - c.p, last.d), p: c.p})
else
newOp.push c
@@ -150,25 +157,25 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
checkValidOp [otherC]
if c.i?
append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')}
append dest, cloneAndModify(c, {p:transformPosition(c.p, otherC, side == 'right')})
else if c.d? # Delete
if otherC.i? # delete vs insert
s = c.d
if c.p < otherC.p
append dest, {d:s[...otherC.p - c.p], p:c.p}
append dest, cloneAndModify(c, {d:s[...otherC.p - c.p]})
s = s[(otherC.p - c.p)..]
if s != ''
append dest, {d:s, p:c.p + otherC.i.length}
append dest, cloneAndModify(c, {d:s, p:c.p + otherC.i.length})
else if otherC.d? # Delete vs delete
if c.p >= otherC.p + otherC.d.length
append dest, {d:c.d, p:c.p - otherC.d.length}
append dest, cloneAndModify(c, {p:c.p - otherC.d.length})
else if c.p + c.d.length <= otherC.p
append dest, c
else
# They overlap somewhere.
newC = {d:'', p:c.p}
newC = cloneAndModify(c, {d:''})
if c.p < otherC.p
newC.d = c.d[...(otherC.p - c.p)]
if c.p + c.d.length > otherC.p + otherC.d.length
@@ -198,18 +205,18 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
if c.p < otherC.p < c.p + c.c.length
offset = otherC.p - c.p
new_c = (c.c[0..(offset-1)] + otherC.i + c.c[offset...])
append dest, {c:new_c, p:c.p, t: c.t}
append dest, cloneAndModify(c, {c:new_c})
else
append dest, {c:c.c, p:transformPosition(c.p, otherC, true), t: c.t}
append dest, cloneAndModify(c, {p:transformPosition(c.p, otherC, true)})
else if otherC.d?
if c.p >= otherC.p + otherC.d.length
append dest, {c:c.c, p:c.p - otherC.d.length, t: c.t}
append dest, cloneAndModify(c, {p:c.p - otherC.d.length})
else if c.p + c.c.length <= otherC.p
append dest, c
else # Delete overlaps comment
# They overlap somewhere.
newC = {c:'', p:c.p, t: c.t}
newC = cloneAndModify(c, {c:''})
if c.p < otherC.p
newC.c = c.c[...(otherC.p - c.p)]
if c.p + c.c.length > otherC.p + otherC.d.length

View File

@@ -1,5 +1,8 @@
load = (EventEmitter) ->
class RangesTracker extends EventEmitter
# This file is shared between document-updater and web, so that the server and client share
# an identical track changes implementation. Do not edit it directly in web or document-updater,
# instead edit it at https://github.com/sharelatex/ranges-tracker, where it has a suite of tests
load = () ->
class RangesTracker
# The purpose of this class is to track a set of inserts and deletes to a document, like
# track changes in Word. We store these as a set of ShareJs style ranges:
# {i: "foo", p: 42} # Insert 'foo' at offset 42
@@ -36,6 +39,7 @@ load = (EventEmitter) ->
# middle of a previous insert by the first user, the original insert will be split into two.
constructor: (@changes = [], @comments = []) ->
@setIdSeed(RangesTracker.generateIdSeed())
@resetDirtyState()
getIdSeed: () ->
return @id_seed
@@ -75,8 +79,15 @@ load = (EventEmitter) ->
comment = @getComment(comment_id)
return if !comment?
@comments = @comments.filter (c) -> c.id != comment_id
@emit "comment:removed", comment
@_markAsDirty comment, "comment", "removed"
moveCommentId: (comment_id, position, text) ->
for comment in @comments
if comment.id == comment_id
comment.op.p = position
comment.op.c = text
@_markAsDirty comment, "comment", "moved"
getChange: (change_id) ->
change = null
for c in @changes
@@ -89,6 +100,18 @@ load = (EventEmitter) ->
change = @getChange(change_id)
return if !change?
@_removeChange(change)
validate: (text) ->
for change in @changes
if change.op.i?
content = text.slice(change.op.p, change.op.p + change.op.i.length)
if content != change.op.i
throw new Error("Change (#{JSON.stringify(change)}) doesn't match text (#{JSON.stringify(content)})")
for comment in @comments
content = text.slice(comment.op.p, comment.op.p + comment.op.c.length)
if content != comment.op.c
throw new Error("Comment (#{JSON.stringify(comment)}) doesn't match text (#{JSON.stringify(content)})")
return true
applyOp: (op, metadata = {}) ->
metadata.ts ?= new Date()
@@ -103,29 +126,37 @@ load = (EventEmitter) ->
@addComment(op, metadata)
else
throw new Error("unknown op type")
applyOps: (ops, metadata = {}) ->
for op in ops
@applyOp(op, metadata)
addComment: (op, metadata) ->
# TODO: Don't allow overlapping comments?
@comments.push comment = {
id: op.t or @newId()
op: # Copy because we'll modify in place
c: op.c
p: op.p
t: op.t
metadata
}
@emit "comment:added", comment
return comment
existing = @getComment(op.t)
if existing?
@moveCommentId(op.t, op.p, op.c)
return existing
else
@comments.push comment = {
id: op.t or @newId()
op: # Copy because we'll modify in place
c: op.c
p: op.p
t: op.t
metadata
}
@_markAsDirty comment, "comment", "added"
return comment
applyInsertToComments: (op) ->
for comment in @comments
if op.p <= comment.op.p
comment.op.p += op.i.length
@emit "comment:moved", comment
@_markAsDirty comment, "comment", "moved"
else if op.p < comment.op.p + comment.op.c.length
offset = op.p - comment.op.p
comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...]
@emit "comment:moved", comment
@_markAsDirty comment, "comment", "moved"
applyDeleteToComments: (op) ->
op_start = op.p
@@ -138,7 +169,7 @@ load = (EventEmitter) ->
if op_end <= comment_start
# delete is fully before comment
comment.op.p -= op_length
@emit "comment:moved", comment
@_markAsDirty comment, "comment", "moved"
else if op_start >= comment_end
# delete is fully after comment, nothing to do
else
@@ -161,12 +192,13 @@ load = (EventEmitter) ->
comment.op.p = Math.min(comment_start, op_start)
comment.op.c = remaining_before + remaining_after
@emit "comment:moved", comment
@_markAsDirty comment, "comment", "moved"
applyInsertToChanges: (op, metadata) ->
op_start = op.p
op_length = op.i.length
op_end = op.p + op_length
undoing = !!op.u
already_merged = false
@@ -184,8 +216,9 @@ load = (EventEmitter) ->
change.op.p += op_length
moved_changes.push change
else if op_start == change_start
# If the insert matches the start of the delete, just remove it from the delete instead
if change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
# If we are undoing, then we want to cancel any existing delete ranges if we can.
# Check if the insert matches the start of the delete, and just remove it from the delete instead if so.
if undoing and change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
change.op.d = change.op.d.slice(op.i.length)
change.op.p += op.i.length
if change.op.d == ""
@@ -203,15 +236,15 @@ load = (EventEmitter) ->
# Only merge inserts if they are from the same user
is_same_user = metadata.user_id == change.metadata.user_id
# If this is an insert op at the end of an existing insert with a delete following, and it cancels out the following
# delete then we shouldn't append it to this insert, but instead only cancel the following delete.
# If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
# an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
# E.g.
# foo|<--- about to insert 'b' here
# inserted 'foo' --^ ^-- deleted 'bar'
# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
next_change = @changes[i+1]
is_op_adjacent_to_next_delete = next_change? and next_change.op.d? and op.p == change_end and next_change.op.p == op.p
will_op_cancel_next_delete = is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
will_op_cancel_next_delete = undoing and is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
# If there is a delete at the start of the insert, and we're inserting
# at the start, we SHOULDN'T merge since the delete acts as a partition.
@@ -281,8 +314,8 @@ load = (EventEmitter) ->
for change in remove_changes
@_removeChange change
if moved_changes.length > 0
@emit "changes:moved", moved_changes
for change in moved_changes
@_markAsDirty change, "change", "moved"
applyDeleteToChanges: (op, metadata) ->
op_start = op.p
@@ -406,8 +439,8 @@ load = (EventEmitter) ->
@_removeChange change
moved_changes = moved_changes.filter (c) -> c != change
if moved_changes.length > 0
@emit "changes:moved", moved_changes
for change in moved_changes
@_markAsDirty change, "change", "moved"
_addOp: (op, metadata) ->
change = {
@@ -427,17 +460,11 @@ load = (EventEmitter) ->
else
return -1
if op.d?
@emit "delete:added", change
else if op.i?
@emit "insert:added", change
@_markAsDirty(change, "change", "added")
_removeChange: (change) ->
@changes = @changes.filter (c) -> c.id != change.id
if change.op.d?
@emit "delete:removed", change
else if change.op.i?
@emit "insert:removed", change
@_markAsDirty change, "change", "removed"
_applyOpModifications: (content, op_modifications) ->
# Put in descending position order, with deleting first if at the same offset
@@ -486,13 +513,32 @@ load = (EventEmitter) ->
previous_change = change
return { moved_changes, remove_changes }
resetDirtyState: () ->
@_dirtyState = {
comment: {
moved: {}
removed: {}
added: {}
}
change: {
moved: {}
removed: {}
added: {}
}
}
getDirtyState: () ->
return @_dirtyState
_markAsDirty: (object, type, action) ->
@_dirtyState[type][action][object.id] = object
_clone: (object) ->
clone = {}
(clone[k] = v for k,v of object)
return clone
if define?
define ["utils/EventEmitter"], load
define [], load
else
EventEmitter = require("events").EventEmitter
module.exports = load(EventEmitter)
module.exports = load()

View File

@@ -27,6 +27,10 @@ define [
layoutToLeft: false
rendererData: {}
loadingThreads: false
newAddCommentUI: false # Test new UI for adding comments; remove afterwards.
if window.location.search.match /new-comments=true/
$scope.reviewPanel.newAddCommentUI = true
window.addEventListener "beforeunload", () ->
collapsedStates = {}
@@ -163,7 +167,11 @@ define [
$scope.$watch (() ->
entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
Object.keys(entries).length
permEntries = {}
for entry, entryData of entries
if entry != "add-comment" or !$scope.reviewPanel.newAddCommentUI
permEntries[entry] = entryData
Object.keys(permEntries).length
), (nEntries) ->
$scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible
@@ -323,6 +331,10 @@ define [
$scope.$broadcast "change:reject", entry_id
event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
$scope.addNewComment = () ->
$scope.$broadcast "comment:start_adding"
$scope.toggleReviewPanel()
$scope.startNewComment = () ->
$scope.$broadcast "comment:select_line"
$timeout () ->

View File

@@ -8,13 +8,16 @@ define [
onStartNew: "&"
onSubmit: "&"
onCancel: "&"
onIndicatorClick: "&"
onIndicatorClick: "&"
layoutToLeft: "="
link: (scope, element, attrs) ->
scope.state =
isAdding: false
content: ""
scope.$on "comment:start_adding", () ->
scope.startNewComment()
scope.startNewComment = () ->
scope.state.isAdding = true
scope.onStartNew()
@@ -31,7 +34,10 @@ define [
if scope.state.content.length > 0
scope.submitNewComment()
scope.submitNewComment = () ->
scope.submitNewComment = (event) ->
# If this is from a blur event from clicking on cancel, ignore it.
if event? and event.type == "blur" and $(event.relatedTarget).hasClass("rp-entry-button-cancel")
return true
scope.onSubmit { content: scope.state.content }
scope.state.isAdding = false
scope.state.content = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,593 +0,0 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals VBArray, PDFJS */
'use strict';
// Initializing PDFJS global object here, it case if we need to change/disable
// some PDF.js features, e.g. range requests
if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
// Checking if the typed arrays are supported
// Support: iOS<6.0 (subarray), IE<10, Android<4.0
(function checkTypedArrayCompatibility() {
if (typeof Uint8Array !== 'undefined') {
// Support: iOS<6.0
if (typeof Uint8Array.prototype.subarray === 'undefined') {
Uint8Array.prototype.subarray = function subarray(start, end) {
return new Uint8Array(this.slice(start, end));
};
Float32Array.prototype.subarray = function subarray(start, end) {
return new Float32Array(this.slice(start, end));
};
}
// Support: Android<4.1
if (typeof Float64Array === 'undefined') {
window.Float64Array = Float32Array;
}
return;
}
function subarray(start, end) {
return new TypedArray(this.slice(start, end));
}
function setArrayOffset(array, offset) {
if (arguments.length < 2) {
offset = 0;
}
for (var i = 0, n = array.length; i < n; ++i, ++offset) {
this[offset] = array[i] & 0xFF;
}
}
function TypedArray(arg1) {
var result, i, n;
if (typeof arg1 === 'number') {
result = [];
for (i = 0; i < arg1; ++i) {
result[i] = 0;
}
} else if ('slice' in arg1) {
result = arg1.slice(0);
} else {
result = [];
for (i = 0, n = arg1.length; i < n; ++i) {
result[i] = arg1[i];
}
}
result.subarray = subarray;
result.buffer = result;
result.byteLength = result.length;
result.set = setArrayOffset;
if (typeof arg1 === 'object' && arg1.buffer) {
result.buffer = arg1.buffer;
}
return result;
}
window.Uint8Array = TypedArray;
window.Int8Array = TypedArray;
// we don't need support for set, byteLength for 32-bit array
// so we can use the TypedArray as well
window.Uint32Array = TypedArray;
window.Int32Array = TypedArray;
window.Uint16Array = TypedArray;
window.Float32Array = TypedArray;
window.Float64Array = TypedArray;
})();
// URL = URL || webkitURL
// Support: Safari<7, Android 4.2+
(function normalizeURLObject() {
if (!window.URL) {
window.URL = window.webkitURL;
}
})();
// Object.defineProperty()?
// Support: Android<4.0, Safari<5.1
(function checkObjectDefinePropertyCompatibility() {
if (typeof Object.defineProperty !== 'undefined') {
var definePropertyPossible = true;
try {
// some browsers (e.g. safari) cannot use defineProperty() on DOM objects
// and thus the native version is not sufficient
Object.defineProperty(new Image(), 'id', { value: 'test' });
// ... another test for android gb browser for non-DOM objects
var Test = function Test() {};
Test.prototype = { get id() { } };
Object.defineProperty(new Test(), 'id',
{ value: '', configurable: true, enumerable: true, writable: false });
} catch (e) {
definePropertyPossible = false;
}
if (definePropertyPossible) {
return;
}
}
Object.defineProperty = function objectDefineProperty(obj, name, def) {
delete obj[name];
if ('get' in def) {
obj.__defineGetter__(name, def['get']);
}
if ('set' in def) {
obj.__defineSetter__(name, def['set']);
}
if ('value' in def) {
obj.__defineSetter__(name, function objectDefinePropertySetter(value) {
this.__defineGetter__(name, function objectDefinePropertyGetter() {
return value;
});
return value;
});
obj[name] = def.value;
}
};
})();
// No XMLHttpRequest#response?
// Support: IE<11, Android <4.0
(function checkXMLHttpRequestResponseCompatibility() {
var xhrPrototype = XMLHttpRequest.prototype;
var xhr = new XMLHttpRequest();
if (!('overrideMimeType' in xhr)) {
// IE10 might have response, but not overrideMimeType
// Support: IE10
Object.defineProperty(xhrPrototype, 'overrideMimeType', {
value: function xmlHttpRequestOverrideMimeType(mimeType) {}
});
}
if ('responseType' in xhr) {
return;
}
// The worker will be using XHR, so we can save time and disable worker.
PDFJS.disableWorker = true;
Object.defineProperty(xhrPrototype, 'responseType', {
get: function xmlHttpRequestGetResponseType() {
return this._responseType || 'text';
},
set: function xmlHttpRequestSetResponseType(value) {
if (value === 'text' || value === 'arraybuffer') {
this._responseType = value;
if (value === 'arraybuffer' &&
typeof this.overrideMimeType === 'function') {
this.overrideMimeType('text/plain; charset=x-user-defined');
}
}
}
});
// Support: IE9
if (typeof VBArray !== 'undefined') {
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType === 'arraybuffer') {
return new Uint8Array(new VBArray(this.responseBody).toArray());
} else {
return this.responseText;
}
}
});
return;
}
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType !== 'arraybuffer') {
return this.responseText;
}
var text = this.responseText;
var i, n = text.length;
var result = new Uint8Array(n);
for (i = 0; i < n; ++i) {
result[i] = text.charCodeAt(i) & 0xFF;
}
return result.buffer;
}
});
})();
// window.btoa (base64 encode function) ?
// Support: IE<10
(function checkWindowBtoaCompatibility() {
if ('btoa' in window) {
return;
}
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.btoa = function windowBtoa(chars) {
var buffer = '';
var i, n;
for (i = 0, n = chars.length; i < n; i += 3) {
var b1 = chars.charCodeAt(i) & 0xFF;
var b2 = chars.charCodeAt(i + 1) & 0xFF;
var b3 = chars.charCodeAt(i + 2) & 0xFF;
var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4);
var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64;
var d4 = i + 2 < n ? (b3 & 0x3F) : 64;
buffer += (digits.charAt(d1) + digits.charAt(d2) +
digits.charAt(d3) + digits.charAt(d4));
}
return buffer;
};
})();
// window.atob (base64 encode function)?
// Support: IE<10
(function checkWindowAtobCompatibility() {
if ('atob' in window) {
return;
}
// https://github.com/davidchambers/Base64.js
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.atob = function (input) {
input = input.replace(/=+$/, '');
if (input.length % 4 === 1) {
throw new Error('bad atob input');
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = input.charAt(idx++);
// character found in table?
// initialize bit storage and add its ascii value
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = digits.indexOf(buffer);
}
return output;
};
})();
// Function.prototype.bind?
// Support: Android<4.0, iOS<6.0
(function checkFunctionPrototypeBindCompatibility() {
if (typeof Function.prototype.bind !== 'undefined') {
return;
}
Function.prototype.bind = function functionPrototypeBind(obj) {
var fn = this, headArgs = Array.prototype.slice.call(arguments, 1);
var bound = function functionPrototypeBindBound() {
var args = headArgs.concat(Array.prototype.slice.call(arguments));
return fn.apply(obj, args);
};
return bound;
};
})();
// HTMLElement dataset property
// Support: IE<11, Safari<5.1, Android<4.0
(function checkDatasetProperty() {
var div = document.createElement('div');
if ('dataset' in div) {
return; // dataset property exists
}
Object.defineProperty(HTMLElement.prototype, 'dataset', {
get: function() {
if (this._dataset) {
return this._dataset;
}
var dataset = {};
for (var j = 0, jj = this.attributes.length; j < jj; j++) {
var attribute = this.attributes[j];
if (attribute.name.substring(0, 5) !== 'data-') {
continue;
}
var key = attribute.name.substring(5).replace(/\-([a-z])/g,
function(all, ch) {
return ch.toUpperCase();
});
dataset[key] = attribute.value;
}
Object.defineProperty(this, '_dataset', {
value: dataset,
writable: false,
enumerable: false
});
return dataset;
},
enumerable: true
});
})();
// HTMLElement classList property
// Support: IE<10, Android<4.0, iOS<5.0
(function checkClassListProperty() {
var div = document.createElement('div');
if ('classList' in div) {
return; // classList property exists
}
function changeList(element, itemName, add, remove) {
var s = element.className || '';
var list = s.split(/\s+/g);
if (list[0] === '') {
list.shift();
}
var index = list.indexOf(itemName);
if (index < 0 && add) {
list.push(itemName);
}
if (index >= 0 && remove) {
list.splice(index, 1);
}
element.className = list.join(' ');
return (index >= 0);
}
var classListPrototype = {
add: function(name) {
changeList(this.element, name, true, false);
},
contains: function(name) {
return changeList(this.element, name, false, false);
},
remove: function(name) {
changeList(this.element, name, false, true);
},
toggle: function(name) {
changeList(this.element, name, true, true);
}
};
Object.defineProperty(HTMLElement.prototype, 'classList', {
get: function() {
if (this._classList) {
return this._classList;
}
var classList = Object.create(classListPrototype, {
element: {
value: this,
writable: false,
enumerable: true
}
});
Object.defineProperty(this, '_classList', {
value: classList,
writable: false,
enumerable: false
});
return classList;
},
enumerable: true
});
})();
// Check console compatibility
// In older IE versions the console object is not available
// unless console is open.
// Support: IE<10
(function checkConsoleCompatibility() {
if (!('console' in window)) {
window.console = {
log: function() {},
error: function() {},
warn: function() {}
};
} else if (!('bind' in console.log)) {
// native functions in IE9 might not have bind
console.log = (function(fn) {
return function(msg) { return fn(msg); };
})(console.log);
console.error = (function(fn) {
return function(msg) { return fn(msg); };
})(console.error);
console.warn = (function(fn) {
return function(msg) { return fn(msg); };
})(console.warn);
}
})();
// Check onclick compatibility in Opera
// Support: Opera<15
(function checkOnClickCompatibility() {
// workaround for reported Opera bug DSK-354448:
// onclick fires on disabled buttons with opaque content
function ignoreIfTargetDisabled(event) {
if (isDisabled(event.target)) {
event.stopPropagation();
}
}
function isDisabled(node) {
return node.disabled || (node.parentNode && isDisabled(node.parentNode));
}
if (navigator.userAgent.indexOf('Opera') !== -1) {
// use browser detection since we cannot feature-check this bug
document.addEventListener('click', ignoreIfTargetDisabled, true);
}
})();
// Checks if possible to use URL.createObjectURL()
// Support: IE
(function checkOnBlobSupport() {
// sometimes IE loosing the data created with createObjectURL(), see #3977
if (navigator.userAgent.indexOf('Trident') >= 0) {
PDFJS.disableCreateObjectURL = true;
}
})();
// Checks if navigator.language is supported
(function checkNavigatorLanguage() {
if ('language' in navigator) {
return;
}
PDFJS.locale = navigator.userLanguage || 'en-US';
})();
(function checkRangeRequests() {
// Safari has issues with cached range requests see:
// https://github.com/mozilla/pdf.js/issues/3260
// Last tested with version 6.0.4.
// Support: Safari 6.0+
var isSafari = Object.prototype.toString.call(
window.HTMLElement).indexOf('Constructor') > 0;
// Older versions of Android (pre 3.0) has issues with range requests, see:
// https://github.com/mozilla/pdf.js/issues/3381.
// Make sure that we only match webkit-based Android browsers,
// since Firefox/Fennec works as expected.
// Support: Android<3.0
var regex = /Android\s[0-2][^\d]/;
var isOldAndroid = regex.test(navigator.userAgent);
// Range requests are broken in Chrome 39 and 40, https://crbug.com/442318
var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent);
if (isSafari || isOldAndroid || isChromeWithRangeBug) {
PDFJS.disableRange = true;
PDFJS.disableStream = true;
}
})();
// Check if the browser supports manipulation of the history.
// Support: IE<10, Android<4.2
(function checkHistoryManipulation() {
// Android 2.x has so buggy pushState support that it was removed in
// Android 3.0 and restored as late as in Android 4.2.
// Support: Android 2.x
if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) {
PDFJS.disableHistory = true;
}
})();
// Support: IE<11, Chrome<21, Android<4.4, Safari<6
(function checkSetPresenceInImageData() {
// IE < 11 will use window.CanvasPixelArray which lacks set function.
if (window.CanvasPixelArray) {
if (typeof window.CanvasPixelArray.prototype.set !== 'function') {
window.CanvasPixelArray.prototype.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
}
} else {
// Old Chrome and Android use an inaccessible CanvasPixelArray prototype.
// Because we cannot feature detect it, we rely on user agent parsing.
var polyfill = false, versionMatch;
if (navigator.userAgent.indexOf('Chrom') >= 0) {
versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
// Chrome < 21 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[2]) < 21;
} else if (navigator.userAgent.indexOf('Android') >= 0) {
// Android < 4.4 lacks the set function.
// Android >= 4.4 will contain Chrome in the user agent,
// thus pass the Chrome check above and not reach this block.
polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent);
} else if (navigator.userAgent.indexOf('Safari') >= 0) {
versionMatch = navigator.userAgent.
match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//);
// Safari < 6 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[1]) < 6;
}
if (polyfill) {
var contextPrototype = window.CanvasRenderingContext2D.prototype;
var createImageData = contextPrototype.createImageData;
contextPrototype.createImageData = function(w, h) {
var imageData = createImageData.call(this, w, h);
imageData.data.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
return imageData;
};
// this closure will be kept referenced, so clear its vars
contextPrototype = null;
}
}
})();
// Support: IE<10, Android<4.0, iOS
(function checkRequestAnimationFrame() {
function fakeRequestAnimationFrame(callback) {
window.setTimeout(callback, 20);
}
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
if (isIOS) {
// requestAnimationFrame on iOS is broken, replacing with fake one.
window.requestAnimationFrame = fakeRequestAnimationFrame;
return;
}
if ('requestAnimationFrame' in window) {
return;
}
window.requestAnimationFrame =
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
fakeRequestAnimationFrame;
})();
(function checkCanvasSizeLimitation() {
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
var isAndroid = /Android/g.test(navigator.userAgent);
if (isIOS || isAndroid) {
// 5MP
PDFJS.maxCanvasPixels = 5242880;
}
})();
// Disable fullscreen support for certain problematic configurations.
// Support: IE11+ (when embedded).
(function checkFullscreenSupport() {
var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 &&
window.parent !== window);
if (isEmbeddedIE) {
PDFJS.disableFullscreen = true;
}
})();
// Provides document.currentScript support
// Support: IE, Chrome<29.
(function checkCurrentScript() {
if ('currentScript' in document) {
return;
}
Object.defineProperty(document, 'currentScript', {
get: function () {
var scripts = document.getElementsByTagName('script');
return scripts[scripts.length - 1];
},
enumerable: true,
configurable: true
});
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,593 +0,0 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals VBArray, PDFJS */
'use strict';
// Initializing PDFJS global object here, it case if we need to change/disable
// some PDF.js features, e.g. range requests
if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
// Checking if the typed arrays are supported
// Support: iOS<6.0 (subarray), IE<10, Android<4.0
(function checkTypedArrayCompatibility() {
if (typeof Uint8Array !== 'undefined') {
// Support: iOS<6.0
if (typeof Uint8Array.prototype.subarray === 'undefined') {
Uint8Array.prototype.subarray = function subarray(start, end) {
return new Uint8Array(this.slice(start, end));
};
Float32Array.prototype.subarray = function subarray(start, end) {
return new Float32Array(this.slice(start, end));
};
}
// Support: Android<4.1
if (typeof Float64Array === 'undefined') {
window.Float64Array = Float32Array;
}
return;
}
function subarray(start, end) {
return new TypedArray(this.slice(start, end));
}
function setArrayOffset(array, offset) {
if (arguments.length < 2) {
offset = 0;
}
for (var i = 0, n = array.length; i < n; ++i, ++offset) {
this[offset] = array[i] & 0xFF;
}
}
function TypedArray(arg1) {
var result, i, n;
if (typeof arg1 === 'number') {
result = [];
for (i = 0; i < arg1; ++i) {
result[i] = 0;
}
} else if ('slice' in arg1) {
result = arg1.slice(0);
} else {
result = [];
for (i = 0, n = arg1.length; i < n; ++i) {
result[i] = arg1[i];
}
}
result.subarray = subarray;
result.buffer = result;
result.byteLength = result.length;
result.set = setArrayOffset;
if (typeof arg1 === 'object' && arg1.buffer) {
result.buffer = arg1.buffer;
}
return result;
}
window.Uint8Array = TypedArray;
window.Int8Array = TypedArray;
// we don't need support for set, byteLength for 32-bit array
// so we can use the TypedArray as well
window.Uint32Array = TypedArray;
window.Int32Array = TypedArray;
window.Uint16Array = TypedArray;
window.Float32Array = TypedArray;
window.Float64Array = TypedArray;
})();
// URL = URL || webkitURL
// Support: Safari<7, Android 4.2+
(function normalizeURLObject() {
if (!window.URL) {
window.URL = window.webkitURL;
}
})();
// Object.defineProperty()?
// Support: Android<4.0, Safari<5.1
(function checkObjectDefinePropertyCompatibility() {
if (typeof Object.defineProperty !== 'undefined') {
var definePropertyPossible = true;
try {
// some browsers (e.g. safari) cannot use defineProperty() on DOM objects
// and thus the native version is not sufficient
Object.defineProperty(new Image(), 'id', { value: 'test' });
// ... another test for android gb browser for non-DOM objects
var Test = function Test() {};
Test.prototype = { get id() { } };
Object.defineProperty(new Test(), 'id',
{ value: '', configurable: true, enumerable: true, writable: false });
} catch (e) {
definePropertyPossible = false;
}
if (definePropertyPossible) {
return;
}
}
Object.defineProperty = function objectDefineProperty(obj, name, def) {
delete obj[name];
if ('get' in def) {
obj.__defineGetter__(name, def['get']);
}
if ('set' in def) {
obj.__defineSetter__(name, def['set']);
}
if ('value' in def) {
obj.__defineSetter__(name, function objectDefinePropertySetter(value) {
this.__defineGetter__(name, function objectDefinePropertyGetter() {
return value;
});
return value;
});
obj[name] = def.value;
}
};
})();
// No XMLHttpRequest#response?
// Support: IE<11, Android <4.0
(function checkXMLHttpRequestResponseCompatibility() {
var xhrPrototype = XMLHttpRequest.prototype;
var xhr = new XMLHttpRequest();
if (!('overrideMimeType' in xhr)) {
// IE10 might have response, but not overrideMimeType
// Support: IE10
Object.defineProperty(xhrPrototype, 'overrideMimeType', {
value: function xmlHttpRequestOverrideMimeType(mimeType) {}
});
}
if ('responseType' in xhr) {
return;
}
// The worker will be using XHR, so we can save time and disable worker.
PDFJS.disableWorker = true;
Object.defineProperty(xhrPrototype, 'responseType', {
get: function xmlHttpRequestGetResponseType() {
return this._responseType || 'text';
},
set: function xmlHttpRequestSetResponseType(value) {
if (value === 'text' || value === 'arraybuffer') {
this._responseType = value;
if (value === 'arraybuffer' &&
typeof this.overrideMimeType === 'function') {
this.overrideMimeType('text/plain; charset=x-user-defined');
}
}
}
});
// Support: IE9
if (typeof VBArray !== 'undefined') {
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType === 'arraybuffer') {
return new Uint8Array(new VBArray(this.responseBody).toArray());
} else {
return this.responseText;
}
}
});
return;
}
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType !== 'arraybuffer') {
return this.responseText;
}
var text = this.responseText;
var i, n = text.length;
var result = new Uint8Array(n);
for (i = 0; i < n; ++i) {
result[i] = text.charCodeAt(i) & 0xFF;
}
return result.buffer;
}
});
})();
// window.btoa (base64 encode function) ?
// Support: IE<10
(function checkWindowBtoaCompatibility() {
if ('btoa' in window) {
return;
}
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.btoa = function windowBtoa(chars) {
var buffer = '';
var i, n;
for (i = 0, n = chars.length; i < n; i += 3) {
var b1 = chars.charCodeAt(i) & 0xFF;
var b2 = chars.charCodeAt(i + 1) & 0xFF;
var b3 = chars.charCodeAt(i + 2) & 0xFF;
var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4);
var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64;
var d4 = i + 2 < n ? (b3 & 0x3F) : 64;
buffer += (digits.charAt(d1) + digits.charAt(d2) +
digits.charAt(d3) + digits.charAt(d4));
}
return buffer;
};
})();
// window.atob (base64 encode function)?
// Support: IE<10
(function checkWindowAtobCompatibility() {
if ('atob' in window) {
return;
}
// https://github.com/davidchambers/Base64.js
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.atob = function (input) {
input = input.replace(/=+$/, '');
if (input.length % 4 === 1) {
throw new Error('bad atob input');
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = input.charAt(idx++);
// character found in table?
// initialize bit storage and add its ascii value
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = digits.indexOf(buffer);
}
return output;
};
})();
// Function.prototype.bind?
// Support: Android<4.0, iOS<6.0
(function checkFunctionPrototypeBindCompatibility() {
if (typeof Function.prototype.bind !== 'undefined') {
return;
}
Function.prototype.bind = function functionPrototypeBind(obj) {
var fn = this, headArgs = Array.prototype.slice.call(arguments, 1);
var bound = function functionPrototypeBindBound() {
var args = headArgs.concat(Array.prototype.slice.call(arguments));
return fn.apply(obj, args);
};
return bound;
};
})();
// HTMLElement dataset property
// Support: IE<11, Safari<5.1, Android<4.0
(function checkDatasetProperty() {
var div = document.createElement('div');
if ('dataset' in div) {
return; // dataset property exists
}
Object.defineProperty(HTMLElement.prototype, 'dataset', {
get: function() {
if (this._dataset) {
return this._dataset;
}
var dataset = {};
for (var j = 0, jj = this.attributes.length; j < jj; j++) {
var attribute = this.attributes[j];
if (attribute.name.substring(0, 5) !== 'data-') {
continue;
}
var key = attribute.name.substring(5).replace(/\-([a-z])/g,
function(all, ch) {
return ch.toUpperCase();
});
dataset[key] = attribute.value;
}
Object.defineProperty(this, '_dataset', {
value: dataset,
writable: false,
enumerable: false
});
return dataset;
},
enumerable: true
});
})();
// HTMLElement classList property
// Support: IE<10, Android<4.0, iOS<5.0
(function checkClassListProperty() {
var div = document.createElement('div');
if ('classList' in div) {
return; // classList property exists
}
function changeList(element, itemName, add, remove) {
var s = element.className || '';
var list = s.split(/\s+/g);
if (list[0] === '') {
list.shift();
}
var index = list.indexOf(itemName);
if (index < 0 && add) {
list.push(itemName);
}
if (index >= 0 && remove) {
list.splice(index, 1);
}
element.className = list.join(' ');
return (index >= 0);
}
var classListPrototype = {
add: function(name) {
changeList(this.element, name, true, false);
},
contains: function(name) {
return changeList(this.element, name, false, false);
},
remove: function(name) {
changeList(this.element, name, false, true);
},
toggle: function(name) {
changeList(this.element, name, true, true);
}
};
Object.defineProperty(HTMLElement.prototype, 'classList', {
get: function() {
if (this._classList) {
return this._classList;
}
var classList = Object.create(classListPrototype, {
element: {
value: this,
writable: false,
enumerable: true
}
});
Object.defineProperty(this, '_classList', {
value: classList,
writable: false,
enumerable: false
});
return classList;
},
enumerable: true
});
})();
// Check console compatibility
// In older IE versions the console object is not available
// unless console is open.
// Support: IE<10
(function checkConsoleCompatibility() {
if (!('console' in window)) {
window.console = {
log: function() {},
error: function() {},
warn: function() {}
};
} else if (!('bind' in console.log)) {
// native functions in IE9 might not have bind
console.log = (function(fn) {
return function(msg) { return fn(msg); };
})(console.log);
console.error = (function(fn) {
return function(msg) { return fn(msg); };
})(console.error);
console.warn = (function(fn) {
return function(msg) { return fn(msg); };
})(console.warn);
}
})();
// Check onclick compatibility in Opera
// Support: Opera<15
(function checkOnClickCompatibility() {
// workaround for reported Opera bug DSK-354448:
// onclick fires on disabled buttons with opaque content
function ignoreIfTargetDisabled(event) {
if (isDisabled(event.target)) {
event.stopPropagation();
}
}
function isDisabled(node) {
return node.disabled || (node.parentNode && isDisabled(node.parentNode));
}
if (navigator.userAgent.indexOf('Opera') !== -1) {
// use browser detection since we cannot feature-check this bug
document.addEventListener('click', ignoreIfTargetDisabled, true);
}
})();
// Checks if possible to use URL.createObjectURL()
// Support: IE
(function checkOnBlobSupport() {
// sometimes IE loosing the data created with createObjectURL(), see #3977
if (navigator.userAgent.indexOf('Trident') >= 0) {
PDFJS.disableCreateObjectURL = true;
}
})();
// Checks if navigator.language is supported
(function checkNavigatorLanguage() {
if ('language' in navigator) {
return;
}
PDFJS.locale = navigator.userLanguage || 'en-US';
})();
(function checkRangeRequests() {
// Safari has issues with cached range requests see:
// https://github.com/mozilla/pdf.js/issues/3260
// Last tested with version 6.0.4.
// Support: Safari 6.0+
var isSafari = Object.prototype.toString.call(
window.HTMLElement).indexOf('Constructor') > 0;
// Older versions of Android (pre 3.0) has issues with range requests, see:
// https://github.com/mozilla/pdf.js/issues/3381.
// Make sure that we only match webkit-based Android browsers,
// since Firefox/Fennec works as expected.
// Support: Android<3.0
var regex = /Android\s[0-2][^\d]/;
var isOldAndroid = regex.test(navigator.userAgent);
// Range requests are broken in Chrome 39 and 40, https://crbug.com/442318
var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent);
if (isSafari || isOldAndroid || isChromeWithRangeBug) {
PDFJS.disableRange = true;
PDFJS.disableStream = true;
}
})();
// Check if the browser supports manipulation of the history.
// Support: IE<10, Android<4.2
(function checkHistoryManipulation() {
// Android 2.x has so buggy pushState support that it was removed in
// Android 3.0 and restored as late as in Android 4.2.
// Support: Android 2.x
if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) {
PDFJS.disableHistory = true;
}
})();
// Support: IE<11, Chrome<21, Android<4.4, Safari<6
(function checkSetPresenceInImageData() {
// IE < 11 will use window.CanvasPixelArray which lacks set function.
if (window.CanvasPixelArray) {
if (typeof window.CanvasPixelArray.prototype.set !== 'function') {
window.CanvasPixelArray.prototype.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
}
} else {
// Old Chrome and Android use an inaccessible CanvasPixelArray prototype.
// Because we cannot feature detect it, we rely on user agent parsing.
var polyfill = false, versionMatch;
if (navigator.userAgent.indexOf('Chrom') >= 0) {
versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
// Chrome < 21 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[2]) < 21;
} else if (navigator.userAgent.indexOf('Android') >= 0) {
// Android < 4.4 lacks the set function.
// Android >= 4.4 will contain Chrome in the user agent,
// thus pass the Chrome check above and not reach this block.
polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent);
} else if (navigator.userAgent.indexOf('Safari') >= 0) {
versionMatch = navigator.userAgent.
match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//);
// Safari < 6 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[1]) < 6;
}
if (polyfill) {
var contextPrototype = window.CanvasRenderingContext2D.prototype;
var createImageData = contextPrototype.createImageData;
contextPrototype.createImageData = function(w, h) {
var imageData = createImageData.call(this, w, h);
imageData.data.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
return imageData;
};
// this closure will be kept referenced, so clear its vars
contextPrototype = null;
}
}
})();
// Support: IE<10, Android<4.0, iOS
(function checkRequestAnimationFrame() {
function fakeRequestAnimationFrame(callback) {
window.setTimeout(callback, 20);
}
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
if (isIOS) {
// requestAnimationFrame on iOS is broken, replacing with fake one.
window.requestAnimationFrame = fakeRequestAnimationFrame;
return;
}
if ('requestAnimationFrame' in window) {
return;
}
window.requestAnimationFrame =
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
fakeRequestAnimationFrame;
})();
(function checkCanvasSizeLimitation() {
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
var isAndroid = /Android/g.test(navigator.userAgent);
if (isIOS || isAndroid) {
// 5MP
PDFJS.maxCanvasPixels = 5242880;
}
})();
// Disable fullscreen support for certain problematic configurations.
// Support: IE11+ (when embedded).
(function checkFullscreenSupport() {
var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 &&
window.parent !== window);
if (isEmbeddedIE) {
PDFJS.disableFullscreen = true;
}
})();
// Provides document.currentScript support
// Support: IE, Chrome<29.
(function checkCurrentScript() {
if ('currentScript' in document) {
return;
}
Object.defineProperty(document, 'currentScript', {
get: function () {
var scripts = document.getElementsByTagName('script');
return scripts[scripts.length - 1];
},
enumerable: true,
configurable: true
});
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

Some files were not shown because too many files have changed in this diff Show More