Просмотр исходного кода

Merge branch 'master' into ns-autocomplete

Nate Stemen 9 лет назад
Родитель
Сommit
fb4182cbb1
54 измененных файлов с 1941 добавлено и 5088 удалено
  1. 1 0
      services/web/.gitignore
  2. 3 0
      services/web/Gruntfile.coffee
  3. 116 0
      services/web/Jenkinsfile
  4. 5 1
      services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee
  5. 21 2
      services/web/app/coffee/Features/Project/ProjectController.coffee
  6. 9 0
      services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
  7. 6 14
      services/web/app/coffee/Features/User/UserCreator.coffee
  8. 4 2
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  9. 1 1
      services/web/app/coffee/models/Project.coffee
  10. 4 0
      services/web/app/coffee/models/User.coffee
  11. 56 31
      services/web/app/views/contact-us-modal.pug
  12. 9 9
      services/web/app/views/layout.pug
  13. 5 4
      services/web/app/views/project/editor.pug
  14. 1 1
      services/web/app/views/project/editor/editor.pug
  15. 3 3
      services/web/app/views/project/editor/hotkeys.pug
  16. 65 15
      services/web/app/views/project/editor/review-panel.pug
  17. 4 2
      services/web/config/settings.defaults.coffee
  18. 23 3
      services/web/npm-shrinkwrap.json
  19. 2 0
      services/web/package.json
  20. 5 4
      services/web/public/coffee/directives/asyncForm.coffee
  21. 2 0
      services/web/public/coffee/ide.coffee
  22. 0 4
      services/web/public/coffee/ide/editor/EditorManager.coffee
  23. 4 7
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  24. 54 7
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
  25. 17 0
      services/web/public/coffee/ide/graphics/services/graphics.coffee
  26. 5 2
      services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee
  27. 22 0
      services/web/public/coffee/ide/preamble/services/preamble.coffee
  28. 99 12
      services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
  29. 7 4
      services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee
  30. 11 3
      services/web/public/coffee/main/contact-us.coffee
  31. 1 1
      services/web/public/coffee/main/project-list/project-list.coffee
  32. BIN
      services/web/public/img/ol-brand/logo-horizontal.png
  33. 18 0
      services/web/public/img/ol-brand/overleaf-o-grey.svg
  34. 10 0
      services/web/public/img/ol-brand/overleaf-o.svg
  35. BIN
      services/web/public/img/teasers/track-changes/per-user-track-changes.gif
  36. BIN
      services/web/public/img/teasers/track-changes/per-user-track-changes.mp4
  37. 1 1
      services/web/public/js/ace-1.2.5/mode-latex.js
  38. 0 655
      services/web/public/js/ace-1.2.5/mode-latex_beta.js
  39. 291 189
      services/web/public/js/ace-1.2.5/worker-latex.js
  40. 0 3207
      services/web/public/js/ace-1.2.5/worker-latex_beta.js
  41. BIN
      services/web/public/ol-favicon.ico
  42. 84 0
      services/web/public/stylesheets/_style_includes.less
  43. 6 5
      services/web/public/stylesheets/app/editor.less
  44. 58 9
      services/web/public/stylesheets/app/editor/review-panel.less
  45. 1 1
      services/web/public/stylesheets/components/navbar.less
  46. 4 0
      services/web/public/stylesheets/components/navs.less
  47. 805 0
      services/web/public/stylesheets/core/_common-variables.less
  48. 37 0
      services/web/public/stylesheets/core/ol-variables.less
  49. 5 804
      services/web/public/stylesheets/core/variables.less
  50. 3 0
      services/web/public/stylesheets/ol-style.less
  51. 1 84
      services/web/public/stylesheets/style.less
  52. 18 0
      services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee
  53. 33 0
      services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee
  54. 1 1
      services/web/test/smoke/coffee/SmokeTests.coffee

+ 1 - 0
services/web/.gitignore

@@ -60,6 +60,7 @@ public/js/services/
 public/js/utils/
 
 public/stylesheets/style.css
+public/stylesheets/ol-style.css
 public/brand/plans.css
 public/minjs/
 

+ 3 - 0
services/web/Gruntfile.coffee

@@ -139,6 +139,9 @@ module.exports = (grunt) ->
 			app:
 				files:
 					"public/stylesheets/style.css": "public/stylesheets/style.less"
+			ol:
+				files:
+					"public/stylesheets/ol-style.css": "public/stylesheets/ol-style.less"
 
 		postcss:
 			options:

+ 116 - 0
services/web/Jenkinsfile

@@ -0,0 +1,116 @@
+pipeline {
+  
+  agent {
+    docker {
+      image 'node:6.9.5'
+      args "-v /var/lib/jenkins/.npm:/tmp/.npm"
+    }
+  }
+  
+  environment  {
+      HOME = "/tmp"
+  }
+  
+  triggers {
+    pollSCM('* * * * *')
+    cron('@daily')
+  }
+  
+  stages {
+    stage('Set up') {
+      steps {
+        // we need to disable logallrefupdates, else git clones during the npm install will require git to lookup the user id
+        // which does not exist in the container's /etc/passwd file, causing the clone to fail.
+        sh 'git config --global core.logallrefupdates false'
+        
+        sh 'rm -rf node_modules/*'
+      }
+    }
+    
+    stage('Clone Dependencies') {
+      steps {
+        sh 'rm -rf public/brand modules'
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'public/brand'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/brand-sharelatex']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'app/views/external'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/external-pages-sharelatex']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/web-sharelatex-modules']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/admin-panel'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/admin-panel']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/groovehq'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/groovehq']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/references-search'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/references-search.git']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/tpr-webmodule'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/tpr-webmodule.git ']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]])
+      }
+    }
+    
+    stage('Install') {
+      steps {
+        sh 'mv app/views/external/robots.txt public/robots.txt'
+        sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html'
+        sh 'npm install'
+        sh 'npm rebuild'
+        sh 'npm install --quiet grunt'
+        sh 'npm install --quiet grunt-cli'
+        sh 'ls -l node_modules/.bin'
+      }
+    }
+
+    stage('Compile') {
+      steps {
+        sh 'node_modules/.bin/grunt compile  --verbose'
+      }
+    }
+
+    stage('Smoke Test') {
+      steps {
+        sh 'node_modules/.bin/grunt compile:smoke_tests'
+      }
+    }
+
+    stage('Minify') {
+      steps {
+        sh 'node_modules/.bin/grunt compile:minify'
+      }
+    }
+    
+    stage('Unit Test') {
+      steps {
+        sh 'env NODE_ENV=development ./node_modules/.bin/grunt test:unit --reporter=tap'
+      }
+    }
+    
+    stage('Package') {
+      steps {
+        sh 'rm -rf ./node_modules/grunt*'
+        sh 'touch build.tar.gz' // Avoid tar warning about files changing during read
+        sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .'
+      }
+    }
+    stage('Publish') {
+      steps {
+        withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") {
+            s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz")
+        }
+      }
+    }
+  }
+  
+  post {
+    failure {
+      mail(from: "${EMAIL_ALERT_FROM}", 
+           to: "${EMAIL_ALERT_TO}", 
+           subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}",
+           body: "Build: ${BUILD_URL}")
+    }
+  }
+  
+
+  // The options directive is for configuration that applies to the whole job.
+  options {
+    // we'd like to make sure remove old builds, so we don't fill up our storage!
+    buildDiscarder(logRotator(numToKeepStr:'50'))
+    
+    // And we'd really like to be sure that this build doesn't hang forever, so let's time it out after:
+    timeout(time: 30, unit: 'MINUTES')
+  }
+}

+ 5 - 1
services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee

@@ -29,8 +29,12 @@ module.exports = AuthenticationManager =
 				callback null, null
 
 	setUserPassword: (user_id, password, callback = (error) ->) ->
-		if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length
+		if (Settings.passwordStrengthOptions?.length?.max? and
+				Settings.passwordStrengthOptions?.length?.max < password.length)
 			return callback("password is too long")
+		if (Settings.passwordStrengthOptions?.length?.min? and
+				Settings.passwordStrengthOptions?.length?.min > password.length)
+			return callback("password is too short")
 
 		bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
 			return callback(error) if error?

+ 21 - 2
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -230,6 +230,24 @@ module.exports = ProjectController =
 						return cb(null, false)
 					else
 						return cb(null, true)
+			showPerUserTCNotice: (cb) ->
+				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-08-09")
+					# Don't show for users who registered after it was released
+					return cb(null, false)
+				timeout = setTimeout cb, 500
+				AnalyticsManager.getLastOccurance user_id, "shown-per-user-tc-notice", (error, event) ->
+					clearTimeout timeout
+					if error?
+						return cb(null, false)
+					else if event?
+						return cb(null, false)
+					else
+						return cb(null, true)
 		}, (err, results)->
 			if err?
 				logger.err err:err, "error getting details for project page"
@@ -237,7 +255,7 @@ module.exports = ProjectController =
 			project = results.project
 			user = results.user
 			subscription = results.subscription
-			showTrackChangesOnboarding = results.showTrackChangesOnboarding
+			{ showTrackChangesOnboarding, showPerUserTCNotice } = results
 
 			daysSinceLastUpdated =  (new Date() - project.lastUpdated) /86400000
 			logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor"
@@ -279,8 +297,9 @@ module.exports = ProjectController =
 						pdfViewer : user.ace.pdfViewer
 						syntaxValidation: user.ace.syntaxValidation
 					}
-					trackChangesEnabled: !!project.track_changes
+					trackChangesState: project.track_changes
 					showTrackChangesOnboarding: !!showTrackChangesOnboarding
+					showPerUserTCNotice: !!showPerUserTCNotice
 					privilegeLevel: privilegeLevel
 					chatUrl: Settings.apis.chat.url
 					anonymous: anonymous

+ 9 - 0
services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee

@@ -62,6 +62,15 @@ module.exports = SubscriptionUpdater =
 			invited_emails: email
 		}, callback
 
+	deleteSubscription: (subscription_id, callback = (error) ->) ->
+		SubscriptionLocator.getSubscription subscription_id, (err, subscription) ->
+			return callback(err) if err?
+			affected_user_ids = [subscription.admin_id].concat(subscription.member_ids or [])
+			logger.log {subscription_id, affected_user_ids}, "deleting subscription and downgrading users"
+			Subscription.remove {_id: ObjectId(subscription_id)}, (err) ->
+				return callback(err) if err?
+				async.mapSeries affected_user_ids, SubscriptionUpdater._setUsersMinimumFeatures, callback
+
 	_createNewSubscription: (adminUser_id, callback)->
 		logger.log adminUser_id:adminUser_id, "creating new subscription"
 		subscription = new Subscription(admin_id:adminUser_id)

+ 6 - 14
services/web/app/coffee/Features/User/UserCreator.coffee

@@ -17,23 +17,15 @@ module.exports = UserCreator =
 	createNewUser: (opts, callback)->
 		logger.log opts:opts, "creating new user"
 		user = new User()
-		user.email = opts.email
-		user.holdingAccount = opts.holdingAccount
-		user.ace.syntaxValidation = true
 
 		username = opts.email.match(/^[^@]*/)
-		if opts.first_name? and opts.first_name.length != 0
-			user.first_name = opts.first_name
-		else if username?
-			user.first_name = username[0]
-		else
-			user.first_name = ""
-
-		if opts.last_name?
-			user.last_name = opts.last_name
-		else
-			user.last_name = ""
+		if !opts.first_name? or opts.first_name == ""
+			opts.first_name = username[0]
 
+		for key, value of opts
+			user[key] = value
+			
+		user.ace.syntaxValidation = true
 		user.featureSwitches?.pdfng = true
 
 		user.save (err)->

+ 4 - 2
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -11,6 +11,7 @@ async = require("async")
 Modules = require "./Modules"
 Url = require "url"
 PackageVersions = require "./PackageVersions"
+htmlEncoder = new require("node-html-encoder").Encoder("numerical")
 fingerprints = {}
 Path = require 'path'
 
@@ -151,9 +152,10 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
 		next()
 
 	webRouter.use (req, res, next)->
-		res.locals.translate = (key, vars = {}) ->
+		res.locals.translate = (key, vars = {}, htmlEncode = false) ->
 			vars.appName = Settings.appName
-			req.i18n.translate(key, vars)
+			str = req.i18n.translate(key, vars)
+			if htmlEncode then htmlEncoder.htmlEncode(str) else str
 		# Don't include the query string parameters, otherwise Google
 		# treats ?nocdn=true as the canonical version
 		res.locals.currentUrl = Url.parse(req.originalUrl).pathname

+ 1 - 1
services/web/app/coffee/models/Project.coffee

@@ -32,7 +32,7 @@ ProjectSchema = new Schema
 	archived          : { type: Boolean }
 	deletedDocs       : [DeletedDocSchema]
 	imageName         : { type: String }
-	track_changes     : { type: Boolean }
+	track_changes     : { type: Object }
 
 ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
 	if project_or_id._id?

+ 4 - 0
services/web/app/coffee/models/User.coffee

@@ -59,6 +59,10 @@ UserSchema = new Schema
 		zotero: Boolean
 	}
 	betaProgram:   { type:Boolean, default: false}
+	overleaf:
+		id: { type: Number }
+		accessToken: { type: String }
+		refreshToken: { type: String }
 
 conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10)
 

+ 56 - 31
services/web/app/views/contact-us-modal.pug

@@ -7,36 +7,61 @@ script(type='text/ng-template', id='supportModalTemplate')
 		) &times;
 		h3 #{translate("contact_us")}
 	.modal-body.contact-us-modal
-		span(ng-show="sent == false")
-			label
-				| #{translate("subject")}
-			.form-group
-				input.field.text.medium.span8.form-control(
-					ng-model="form.subject",
-					ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
-					maxlength='255',
-					tabindex='1',
-					onkeyup='')
-			.contact-suggestions(ng-show="suggestions.length")
-				p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })} 
-				ul.contact-suggestion-list
-					li(ng-repeat="suggestion in suggestions")
-						a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
-							span(ng-bind-html="suggestion.name")
-							i.fa.fa-angle-right
-			label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
-				| #{translate("email")}
-			.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
-				input.field.text.medium.span8.form-control(ng-model="form.email",  ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
-			label#title12.desc
-				| #{translate("project_url")} (#{translate("optional")})
-			.form-group
-				input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
-			label.desc
-				| #{translate("contact_message_label")}
-			.form-group
-				textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
-			.form-group.text-center
-				input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us"))
+		form(name="contactForm")
+			span(ng-show="sent == false")
+				.alert.alert-danger(ng-show="error") Something went wrong sending your request :(
+				label
+					| #{translate("subject")}
+				.form-group
+					input.field.text.medium.span8.form-control(
+						name="subject",
+						required
+						ng-model="form.subject",
+						ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
+						maxlength='255',
+						tabindex='1',
+						onkeyup='')
+				.contact-suggestions(ng-show="suggestions.length")
+					p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })} 
+					ul.contact-suggestion-list
+						li(ng-repeat="suggestion in suggestions")
+							a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
+								span(ng-bind-html="suggestion.name")
+								i.fa.fa-angle-right
+				label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
+					| #{translate("email")}
+				.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
+					input.field.text.medium.span8.form-control(
+						name="email",
+						required
+						ng-model="form.email", 
+						ng-init="form.email = '"+getUserEmail()+"'",
+						type='email', spellcheck='false',
+						value='',
+						maxlength='255',
+						tabindex='2')
+				label#title12.desc
+					| #{translate("project_url")} (#{translate("optional")})
+				.form-group
+					input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
+				label.desc
+					| #{translate("contact_message_label")}
+				.form-group
+					textarea.field.text.medium.span8.form-control(
+						name="body",
+						required
+						ng-model="form.message",
+						type='text',
+						value='',
+						tabindex='4',
+						onkeyup=''
+					)
+				.form-group.text-center
+					input.btn-success.btn.btn-lg(
+						type='submit',
+						ng-disabled="contactForm.$invalid || sending",
+						ng-click="contactUs()"
+						value=translate("contact_us")
+					)
 		span(ng-show="sent")
 			p #{translate("request_sent_thank_you")}

+ 9 - 9
services/web/app/views/layout.pug

@@ -14,16 +14,16 @@ html(itemscope, itemtype='http://schema.org/Product')
 		
 
 		-if (typeof(title) == "undefined")
-			title= 'ShareLaTeX, '+ translate("online_latex_editor") 
+			title= settings.appName + ', '+ translate("online_latex_editor") 
 		-else
-			title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor") 
+			title= translate(title) + ' - ' + settings.appName + ', ' + translate("online_latex_editor") 
 
-		link(rel="icon", href="/favicon.ico")
-		link(rel="icon", sizes="192x192", href="/touch-icon-192x192.png")
-		link(rel="apple-touch-icon-precomposed", href="/apple-touch-icon-precomposed.png")
-		link(rel="mask-icon", href="/mask-favicon.svg", color="#a93529")
+		link(rel="icon", href="/" + settings.brandPrefix + "favicon.ico")
+		link(rel="icon", sizes="192x192", href="/" + settings.brandPrefix + "touch-icon-192x192.png")
+		link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png")
+		link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529")
 			
-		link(rel='stylesheet', href=buildCssPath('/style.css'))
+		link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css"))
 
 		block _headLinks
 
@@ -33,14 +33,14 @@ html(itemscope, itemtype='http://schema.org/Product')
 					link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode)
 
 
-		meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
+		meta(itemprop="name", content=settings.appName + ", the Online LaTeX Editor")
 		
 		-if (typeof(meta) == "undefined")
 			meta(itemprop="description", name="description", content=translate("site_description"))
 		-else
 			meta(itemprop="description", name="description" , content=meta)
 		
-		meta(itemprop="image", name="image", content="https://www.sharelatex.com/favicon.ico")
+		meta(itemprop="image", name="image", content="/" + settings.brandPrefix + "favicon.ico")
 
 		- if (typeof(gaToken) != "undefined")
 			script(type='text/javascript').

+ 5 - 4
services/web/app/views/project/editor.pug

@@ -8,8 +8,8 @@ block vars
 block content
 	.editor(ng-controller="IdeController").full-size
 		.loading-screen(ng-if="state.loading")
-			.loading-screen-lion-container
-				.loading-screen-lion(
+			.loading-screen-brand-container
+				.loading-screen-brand(
 					style="height: 20%;"
 					ng-style="{ 'height': state.load_progress + '%' }"
 				)
@@ -106,7 +106,7 @@ block requirejs
 	//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
 	//- and doesn't prematurely end the script tag.
 	script#data(type="application/json").
-		!{JSON.stringify({userSettings: userSettings, user: user}).replace(/\//g, '\\/')}
+		!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')}
 
 	script(type="text/javascript").
 		window.data = JSON.parse($("#data").text());
@@ -118,8 +118,9 @@ block requirejs
 		window.csrfToken = "!{csrfToken}";
 		window.anonymous = #{anonymous};
 		window.maxDocLength = #{maxDocLength};
-		window.trackChangesEnabled = #{trackChangesEnabled};
+		window.trackChangesState = data.trackChangesState;
 		window.showTrackChangesOnboarding = #{!!showTrackChangesOnboarding};
+		window.showPerUserTCNotice = #{!!showPerUserTCNotice};
 		window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
 		window.requirejs = {
 			"paths" : {

+ 1 - 1
services/web/app/views/project/editor/editor.pug

@@ -82,7 +82,7 @@ div.full-size(
 			i.fa.fa-long-arrow-right
 		br
 		a.btn.btn-default.btn-xs(
-			tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'"
+			tooltip-html="'"+translate('go_to_pdf_location_in_code', {}, true)+"'"
 			tooltip-placement="right"
 			tooltip-append-to-body="true"
 			ng-click="syncToCode()"

+ 3 - 3
services/web/app/views/project/editor/hotkeys.pug

@@ -90,8 +90,8 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
 					span.combination Ctrl + Space 
 					span.description Search References
 		
-		h3 #{translate("review")}
-		.row
+		h3(ng-if="trackChangesVisible") #{translate("review")}
+		.row(ng-if="trackChangesVisible")
 			.col-xs-4
 				.hotkey
 					span.combination {{ctrl}} + J
@@ -108,4 +108,4 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
 	.modal-footer
 		button.btn.btn-default(
 			ng-click="cancel()"
-		) #{translate("ok")}
+		) #{translate("ok")}

+ 65 - 15
services/web/app/views/project/editor/review-panel.pug

@@ -46,21 +46,40 @@
 			is-loading="reviewPanel.dropdown.loading"
 			permissions="permissions"
 		)
-		span.review-panel-toolbar-label(ng-if="permissions.write")
-			span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")}
-			span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")}
-			review-panel-toggle(
-				ng-if="editor.wantTrackChanges == editor.trackChanges"
-				ng-model="editor.wantTrackChanges"
-				on-toggle="toggleTrackChanges"
-				disabled="!project.features.trackChanges"
-				on-disabled-click="openTrackChangesUpgradeModal"
+		span.review-panel-toolbar-label
+			span.review-panel-toolbar-icon-on(
+				ng-if="editor.wantTrackChanges === true"
 			)
-		span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write")
-			span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")}
-			span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")}
-		span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges")
-			i.fa.fa-spin.fa-spinner
+				i.fa.fa-circle
+			span(ng-click="toggleFullTCStateCollapse();")
+				span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")}
+				span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")}
+				span.rp-tc-state-collapse(
+					ng-class="{ 'rp-tc-state-collapse-on': reviewPanel.fullTCStateCollapsed }"
+				)
+					i.fa.fa-angle-down
+		ul.rp-tc-state(
+			review-panel-collapse-height="reviewPanel.fullTCStateCollapsed"
+		)
+			li.rp-tc-state-item.rp-tc-state-item-everyone
+				span.rp-tc-state-item-name !{translate("tc_everyone")}
+				review-panel-toggle(
+					ng-model="reviewPanel.trackChangesOnForEveryone"
+					on-toggle="toggleTrackChangesForEveryone(isOn);"
+					disabled="!project.features.trackChanges || !permissions.write"
+				)
+			li.rp-tc-state-item(
+				ng-repeat="member in reviewPanel.formattedProjectMembers"
+			)
+				span.rp-tc-state-item-name(
+					ng-class="{ 'rp-tc-state-item-name-disabled' : reviewPanel.trackChangesOnForEveryone}"
+					style="color: hsl({{ member.hue }}, 70%, 40%);"
+				) {{ member.name }}
+				review-panel-toggle(
+					ng-model="reviewPanel.trackChangesState[member.id].value"
+					on-toggle="toggleTrackChangesForUser(isOn, member.id);"
+					disabled="reviewPanel.trackChangesOnForEveryone || !project.features.trackChanges || !permissions.write"
+				)
 
 	.rp-entry-list(
 		review-panel-sorted
@@ -494,7 +513,7 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
 	.modal-body
 		.teaser-video-container
 			video.teaser-video(autoplay, loop)
-				source(src="/img/teasers/track-changes/teaser-track-changes.mp4", type="video/mp4")
+				source(ng-src="{{ '/img/teasers/track-changes/teaser-track-changes.mp4' }}", type="video/mp4")
 				img(src="/img/teasers/track-changes/teaser-track-changes.gif")
 		
 		h4.teaser-title #{translate("see_changes_in_your_documents_live")}
@@ -532,6 +551,37 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
 		)
 			span #{translate("close")}
 
+script(type="text/ng-template", id="perUserTCNoticeModalTemplate")
+	.modal-header
+		button.close(
+			type="button"
+			data-dismiss="modal"
+			ng-click="$close()"
+		) &times;
+		h3 #{translate("per_user_tc_title")}
+	.modal-body
+		.teaser-video-container
+			video.teaser-video(autoplay, loop)
+				source(ng-src="{{ '/img/teasers/track-changes/per-user-track-changes.mp4' }}", type="video/mp4")
+				img(src="/img/teasers/track-changes/per-user-track-changes.gif")
+		h4.teaser-title #{translate("you_can_use_per_user_tc")}
+		.row
+			.col-md-8.col-md-offset-2
+				ul.list-unstyled
+					li
+						i.fa.fa-check &nbsp;
+						| #{translate("turn_tc_on_individuals")}
+					
+					li
+						i.fa.fa-check &nbsp;
+						| #{translate("keep_tc_on_like_before")}
+
+	.modal-footer()
+		button.btn.btn-default(
+			ng-click="$close()"
+		)
+			span #{translate("close")}
+
 script(type="text/ng-template", id="bulkActionsModalTemplate")
 	.modal-header
 		button.close(

+ 4 - 2
services/web/config/settings.defaults.coffee

@@ -226,8 +226,8 @@ module.exports = settings =
 	# passwordStrengthOptions:
 	# 	pattern: "aA$3"
 	# 	length:
-	# 		min: 1
-	# 		max: 10
+	# 		min: 6
+	# 		max: 128
 
 	# Email support
 	# -------------
@@ -343,6 +343,8 @@ module.exports = settings =
 
 	appName: "ShareLaTeX (Community Edition)"
 	adminEmail: "placeholder@example.com"
+	
+	brandPrefix: "" # Set to 'ol-' for overleaf styles
 
 	nav:
 		title: "ShareLaTeX Community Edition"

+ 23 - 3
services/web/npm-shrinkwrap.json

@@ -1509,7 +1509,7 @@
         },
         "minimatch": {
           "version": "3.0.4",
-          "from": "minimatch@^3.0.4",
+          "from": "minimatch@^3.0.2",
           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
           "dev": true
         },
@@ -1678,7 +1678,7 @@
     },
     "iconv-lite": {
       "version": "0.2.11",
-      "from": "iconv-lite@>=0.2.11 <0.3.0",
+      "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
     },
     "ieee754": {
@@ -1698,7 +1698,7 @@
     },
     "inherits": {
       "version": "2.0.3",
-      "from": "inherits@>=2.0.1 <2.1.0",
+      "from": "inherits@>=2.0.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
     },
     "ini": {
@@ -2728,6 +2728,11 @@
       "from": "node-forge@0.2.24",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.2.24.tgz"
     },
+    "node-html-encoder": {
+      "version": "0.0.2",
+      "from": "node-html-encoder@0.0.2",
+      "resolved": "https://registry.npmjs.org/node-html-encoder/-/node-html-encoder-0.0.2.tgz"
+    },
     "node-pre-gyp": {
       "version": "0.6.30",
       "from": "node-pre-gyp@0.6.30",
@@ -2842,6 +2847,11 @@
       "from": "number-is-nan@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz"
     },
+    "oauth": {
+      "version": "0.9.15",
+      "from": "oauth@>=0.9.0 <0.10.0",
+      "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz"
+    },
     "oauth-sign": {
       "version": "0.8.2",
       "from": "oauth-sign@>=0.8.1 <0.9.0",
@@ -3065,6 +3075,11 @@
       "from": "passport-local@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz"
     },
+    "passport-oauth2": {
+      "version": "1.4.0",
+      "from": "passport-oauth2@latest",
+      "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz"
+    },
     "passport-saml": {
       "version": "0.15.0",
       "from": "passport-saml@>=0.15.0 <0.16.0",
@@ -4287,6 +4302,11 @@
       "from": "uid-safe@2.1.4",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz"
     },
+    "uid2": {
+      "version": "0.0.3",
+      "from": "uid2@>=0.0.0 <0.1.0",
+      "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz"
+    },
     "underscore": {
       "version": "1.6.0",
       "from": "underscore@1.6.0",

+ 2 - 0
services/web/package.json

@@ -41,6 +41,7 @@
     "mongojs": "2.4.0",
     "mongoose": "4.11.4",
     "multer": "^0.1.8",
+    "node-html-encoder": "0.0.2",
     "nodemailer": "2.1.0",
     "nodemailer-sendgrid-transport": "^0.2.0",
     "nodemailer-ses-transport": "^1.3.0",
@@ -48,6 +49,7 @@
     "passport": "^0.3.2",
     "passport-ldapauth": "^0.6.0",
     "passport-local": "^1.0.0",
+    "passport-oauth2": "^1.4.0",
     "passport-saml": "^0.15.0",
     "pug": "^2.0.0-beta6",
     "redis": "0.10.1",

+ 5 - 4
services/web/public/coffee/directives/asyncForm.coffee

@@ -103,8 +103,8 @@ define [
 			defaultPasswordOpts =
 				pattern: ""
 				length:
-					min: 1
-					max: 50
+					min: 6
+					max: 128
 				allowEmpty: false
 				allowAnyChars: false
 				isMasked: true
@@ -127,8 +127,6 @@ define [
 			[asyncFormCtrl, ngModelCtrl] = ctrl
 
 			ngModelCtrl.$parsers.unshift (modelValue) ->
-				
-			
 				isValid = passField.validatePass()
 				email = asyncFormCtrl.getEmail() || window.usersEmail
 				if !isValid
@@ -141,5 +139,8 @@ define [
 				if opts.length.max? and modelValue.length == opts.length.max
 					isValid = false
 					scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached"
+				if opts.length.min? and modelValue.length < opts.length.min
+					isValid = false
+					scope.complexPasswordErrorMessage = "Password too short, minimum #{opts.length.min}"
 				ngModelCtrl.$setValidity('complexPassword', isValid)
 				return modelValue

+ 2 - 0
services/web/public/coffee/ide.coffee

@@ -90,6 +90,8 @@ define [
 		$scope.chat = {}
 
 		ide.toggleReviewPanel = $scope.toggleReviewPanel = () ->
+			if !$scope.project.features.trackChangesVisible
+				return
 			$scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
 			event_tracking.sendMB "rp-toggle-panel", { value : $scope.ui.reviewPanelOpen }
 

+ 0 - 4
services/web/public/coffee/ide/editor/EditorManager.coffee

@@ -37,10 +37,6 @@ define [
 			@$scope.$watch "editor.wantTrackChanges", (value) =>
 				return if !value?
 				@_syncTrackChangesState(@$scope.editor.sharejs_doc)
-			
-			@$scope.$watch "project.features.trackChanges", (trackChangesFeature) =>
-				return if !trackChangesFeature?
-				@$scope.editor.wantTrackChanges = window.trackChangesEnabled and trackChangesFeature
 
 		autoOpenDoc: () ->
 			open_doc_id = 

+ 4 - 7
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -11,6 +11,8 @@ define [
 	"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
 	"ide/editor/directives/aceEditor/labels/LabelsManager"
 	"ide/labels/services/labels"
+	"ide/graphics/services/graphics"
+	"ide/preamble/services/preamble"
 ], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) ->
 	EditSession = ace.require('ace/edit_session').EditSession
 	ModeList = ace.require('ace/ext/modelist')
@@ -33,9 +35,8 @@ define [
 			url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
 			return url
 
-	App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels) ->
+	App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, graphics, preamble) ->
 		monkeyPatchSearch($rootScope, $compile)
-		
 
 		return  {
 			scope: {
@@ -102,7 +103,7 @@ define [
 				cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
 				trackChangesManager   = new TrackChangesManager(scope, editor, element)
 				labelsManager = new LabelsManager(scope, editor, element, labels)
-				autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager)
+				autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager, graphics, preamble)
 
 
 				# Prevert Ctrl|Cmd-S from triggering save dialog
@@ -346,10 +347,6 @@ define [
 					catch
 						mode = "ace/mode/plain_text"
 
-					# Give beta users the next release of the syntax checker
-					if mode is "ace/mode/latex" and window.user?.betaProgram
-						mode = "ace/mode/latex_beta"
-
 					# create our new session
 					session = new EditSession(lines, mode)
 

+ 54 - 7
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee

@@ -17,7 +17,7 @@ define [
 		commandFragment?.match(/\\(\w+)\{/)?[1]
 
 	class AutoCompleteManager
-		constructor: (@$scope, @editor, @element, @labelsManager) ->
+		constructor: (@$scope, @editor, @element, @labelsManager, @graphics, @preamble) ->
 			@suggestionManager = new CommandManager()
 
 			@monkeyPatchAutocomplete()
@@ -44,6 +44,37 @@ define [
 
 			SnippetCompleter = new EnvironmentManager()
 
+			Graphics = @graphics
+			Preamble = @preamble
+			GraphicsCompleter =
+				getCompletions: (editor, session, pos, prefix, callback) ->
+					upToCursorRange = new Range(pos.row, 0, pos.row, pos.column)
+					lineUpToCursor = editor.getSession().getTextRange(upToCursorRange)
+					commandFragment = getLastCommandFragment(lineUpToCursor)
+					if commandFragment
+						match = commandFragment.match(/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/)
+						if match
+							beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
+							lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
+							needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/)
+							commandName = match[1]
+							currentArg = match[3]
+							graphicsPaths = Preamble.getGraphicsPaths()
+							result = []
+							for graphic in Graphics.getGraphicsFiles()
+								path = graphic.path
+								for graphicsPath in graphicsPaths
+									if path.indexOf(graphicsPath) == 0
+										path = path.slice(graphicsPath.length)
+										break
+								result.push {
+									caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
+									value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
+									meta: "graphic",
+									score: 50
+								}
+							callback null, result
+
 			labelsManager = @labelsManager
 			LabelsCompleter =
 				getCompletions: (editor, session, pos, prefix, callback) ->
@@ -113,11 +144,12 @@ define [
 								callback null, result
 
 			@editor.completers = [
-								@suggestionManager,
-								SnippetCompleter,
-								ReferencesCompleter,
-								LabelsCompleter
-							]
+				@suggestionManager,
+				SnippetCompleter,
+				ReferencesCompleter,
+				LabelsCompleter,
+				GraphicsCompleter
+			]
 
 		disable: () ->
 			@editor.setOptions({
@@ -250,7 +282,22 @@ define [
 						editor.completer.autoSelect = true
 						editor.completer.showPopup(editor)
 						editor.completer.cancelContextMenu()
-						$(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'})
+						container = $(editor.completer.popup?.container)
+						container.css({'font-size': @$scope.fontSize + 'px'})
+						# Dynamically set width of autocomplete popup
+						if filtered = editor?.completer?.completions?.filtered
+							longestCaption = _.max(filtered.map( (c) -> c.caption.length ))
+							longestMeta = _.max(filtered.map( (c) -> c.meta.length ))
+							charWidth = editor.renderer.characterWidth
+							# between 280 and 700 px
+							width = Math.max(
+								Math.min(
+									Math.round(longestCaption*charWidth + longestMeta*charWidth + 5*charWidth),
+									700
+								),
+								280
+							)
+							container.css({width: "#{width}px"})
 						if editor.completer?.completions?.filtered?.length == 0
 							editor.completer.detach()
 					bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"

+ 17 - 0
services/web/public/coffee/ide/graphics/services/graphics.coffee

@@ -0,0 +1,17 @@
+define [
+	"base"
+], (App) ->
+
+	App.factory 'graphics', (ide) ->
+
+		Graphics =
+			getGraphicsFiles: () ->
+				graphicsFiles = []
+				ide.fileTreeManager.forEachEntity (entity, folder, path) ->
+					if entity.type == 'file' && entity?.name?.match?(/.*\.(png|jpg|jpeg|pdf|eps)/)
+						cloned = _.clone(entity)
+						cloned.path = path
+						graphicsFiles.push cloned
+				return graphicsFiles
+
+		return Graphics

+ 5 - 2
services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee

@@ -10,13 +10,16 @@ define [
 				templateUrl: "hotkeysModalTemplate"
 				controller:  "HotkeysModalController"
 				size: "lg"
+				resolve:
+					trackChangesVisible: () -> $scope.project.features.trackChangesVisible
 			}
 		
-	App.controller "HotkeysModalController", ($scope, $modalInstance)->
+	App.controller "HotkeysModalController", ($scope, $modalInstance, trackChangesVisible)->
+		$scope.trackChangesVisible = trackChangesVisible
 		if ace.require("ace/lib/useragent").isMac
 			$scope.ctrl = "Cmd"
 		else
 			$scope.ctrl = "Ctrl"
 		
 		$scope.cancel = () ->
-			$modalInstance.dismiss()
+			$modalInstance.dismiss()

+ 22 - 0
services/web/public/coffee/ide/preamble/services/preamble.coffee

@@ -0,0 +1,22 @@
+define [
+	"base"
+], (App) ->
+
+	App.factory 'preamble', (ide) ->
+
+		Preamble =
+			getPreambleText: () ->
+				text = ide.editorManager.getCurrentDocValue().slice(0, 5000)
+				preamble = text.match(/([^]*)^\\begin\{document\}/m)?[1] || ""
+				return preamble
+
+			getGraphicsPaths: () ->
+				preamble = Preamble.getPreambleText()
+				graphicsPathsArgs = preamble.match(/\\graphicspath\{(.*)\}/)?[1] || ""
+				paths = []
+				re = /\{([^}]*)\}/g
+				while match = re.exec(graphicsPathsArgs)
+					paths.push(match[1])
+				return paths
+
+		return Preamble

+ 99 - 12
services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee

@@ -11,7 +11,13 @@ define [
 			CUR_FILE : "cur_file"
 			OVERVIEW : "overview"
 
+		$scope.UserTCSyncState = UserTCSyncState =
+			SYNCED  : "synced"
+			PENDING : "pending"
+
 		$scope.reviewPanel =
+			trackChangesState: {}
+			trackChangesOnForEveryone: false
 			entries: {}
 			resolvedComments: {}
 			hasEntries: false
@@ -25,6 +31,8 @@ define [
 			commentThreads: {}
 			resolvedThreadIds: {}
 			rendererData: {}
+			formattedProjectMembers: {}
+			fullTCStateCollapsed: true
 			loadingThreads: false
 			# All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids
 			# will be present. The length of this array will differ from the count below (see explanation).
@@ -32,6 +40,7 @@ define [
 			# A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
 			# as only one.
 			nVisibleSelectedChanges: 0
+			showPerUserTCNotice: window.showPerUserTCNotice
 
 		window.addEventListener "beforeunload", () ->
 			collapsedStates = {}
@@ -59,6 +68,15 @@ define [
 			if !visible
 				$scope.ui.reviewPanelOpen = false
 
+		$scope.$watch "project.members", (members) ->
+			$scope.reviewPanel.formattedProjectMembers = {}
+			if $scope.project?.owner?
+				$scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] = formatUser($scope.project.owner)
+			if $scope.project?.members?
+				for member in members
+					if member.privileges == "readAndWrite"
+						$scope.reviewPanel.formattedProjectMembers[member._id] = formatUser(member)
+
 		$scope.commentState =
 			adding: false
 			content: ""
@@ -416,6 +434,8 @@ define [
 			$scope.toggleReviewPanel()
 
 		$scope.addNewCommentFromKbdShortcut = () ->
+			if !$scope.project.features.trackChangesVisible
+				return
 			$scope.$broadcast "comment:select_line"
 			if !$scope.ui.reviewPanelOpen
 				$scope.toggleReviewPanel()
@@ -565,24 +585,83 @@ define [
 						
 		$scope.gotoEntry = (doc_id, entry) ->
 			ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset })
-		
-		$scope.toggleTrackChanges = (value) ->
+
+		$scope.toggleFullTCStateCollapse = () ->
 			if $scope.project.features.trackChanges
-				$scope.editor.wantTrackChanges = value
-				$http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value}
-				event_tracking.sendMB "rp-trackchanges-toggle", { value }
+				if $scope.reviewPanel.showPerUserTCNotice
+					$scope.openPerUserTCNoticeModal()
+				$scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed
 			else
 				$scope.openTrackChangesUpgradeModal()
 
-		$scope.toggleTrackChangesFromKbdShortcut = () ->
-			if $scope.editor.wantTrackChanges
-				$scope.toggleTrackChanges false
-			else 
-				$scope.toggleTrackChanges true
+		_setUserTCState = (userId, newValue, isLocal = false) ->
+			$scope.reviewPanel.trackChangesState[userId] ?= {}
+			state = $scope.reviewPanel.trackChangesState[userId]
+
+			if !state.syncState? or state.syncState == UserTCSyncState.SYNCED
+				state.value = newValue
+				state.syncState = UserTCSyncState.SYNCED
+			else if state.syncState == UserTCSyncState.PENDING and state.value == newValue
+				state.syncState = UserTCSyncState.SYNCED
+			else if isLocal
+				state.value = newValue
+				state.syncState = UserTCSyncState.PENDING
+			
+			if userId == ide.$scope.user.id
+				$scope.editor.wantTrackChanges = newValue		
+
+		_setEveryoneTCState = (newValue, isLocal = false) ->
+			$scope.reviewPanel.trackChangesOnForEveryone = newValue
+			for member in $scope.project.members
+				_setUserTCState(member._id, newValue, isLocal)
+			_setUserTCState($scope.project.owner._id, newValue, isLocal)
+
+		applyClientTrackChangesStateToServer = () ->
+			if $scope.reviewPanel.trackChangesOnForEveryone
+				data = {on : true}
+			else
+				data = {on_for: {}}
+				for userId, userState of $scope.reviewPanel.trackChangesState
+					data.on_for[userId] = userState.value
+			data._csrf = window.csrfToken
+			$http.post "/project/#{$scope.project_id}/track_changes", data
+
+		applyTrackChangesStateToClient = (state) ->
+			if typeof state is "boolean"
+				_setEveryoneTCState state
+			else
+				$scope.reviewPanel.trackChangesOnForEveryone = false
+				for member in $scope.project.members
+					_setUserTCState(member._id, state[member._id] ? false)
+				_setUserTCState($scope.project.owner._id, state[$scope.project.owner._id] ? false)
 		
-		ide.socket.on "toggle-track-changes", (value) ->
+		$scope.toggleTrackChangesForEveryone = (onForEveryone) ->
+			_setEveryoneTCState onForEveryone, true
+			applyClientTrackChangesStateToServer()
+	
+		$scope.toggleTrackChangesForUser = (onForUser, userId) ->
+			_setUserTCState userId, onForUser, true
+			applyClientTrackChangesStateToServer()				
+
+		ide.socket.on "toggle-track-changes", (state) ->
 			$scope.$apply () ->
-				$scope.editor.wantTrackChanges = value
+				applyTrackChangesStateToClient(state)
+
+		$scope.toggleTrackChangesFromKbdShortcut = () ->
+			if !$scope.project.features.trackChangesVisible
+				return
+			$scope.toggleTrackChangesForUser !$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, ide.$scope.user.id
+
+		_inited = false
+		ide.$scope.$on "project:joined", () ->
+			return if _inited
+			project = ide.$scope.project
+			if project.features.trackChanges
+				window.trackChangesState ?= false
+				applyTrackChangesStateToClient(window.trackChangesState)
+			else
+				applyTrackChangesStateToClient(false)
+			_inited = true
 
 		_refreshingRangeUsers = false
 		_refreshedForUserIds = {}
@@ -676,3 +755,11 @@ define [
 				controller: "TrackChangesUpgradeModalController"
 				scope: $scope.$new()
 			}
+
+		$scope.openPerUserTCNoticeModal = () ->
+			$scope.reviewPanel.showPerUserTCNotice = false
+			$modal.open({
+				templateUrl: "perUserTCNoticeModalTemplate"
+			}).result.finally () ->
+				event_tracking.sendMB "shown-per-user-tc-notice"
+

+ 7 - 4
services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee

@@ -4,20 +4,23 @@ define [
 	App.directive "reviewPanelToggle", () ->
 		restrict: "E"
 		scope: 
-			onToggle: '='
+			onToggle: '&'
 			ngModel: '='
+			valWhenUndefined: '=?'
 			disabled: '=?'
-			onDisabledClick: '=?'
+			onDisabledClick: '&?'
 		link: (scope) ->
 			if !scope.disabled?
 				scope.disabled = false
 			scope.onChange = (args...) ->
-				scope.onToggle(scope.localModel)
+				scope.onToggle({ isOn: scope.localModel })
 			scope.handleClick = () ->
-				if scope.disabled
+				if scope.disabled and scope.onDisabledClick?
 					scope.onDisabledClick()
 			scope.localModel = scope.ngModel
 			scope.$watch "ngModel", (value) ->
+				if scope.valWhenUndefined? and !value?
+					value = scope.valWhenUndefined
 				scope.localModel = value
 
 		template: """

+ 11 - 3
services/web/public/coffee/main/contact-us.coffee

@@ -30,7 +30,7 @@ define [
 				$scope.suggestions = suggestions
 
 		$scope.contactUs = ->
-			if !$scope.form.email?
+			if !$scope.form.email? or $scope.form.email == ""
 				console.log "email not set"
 				return
 			$scope.sending = true
@@ -46,8 +46,16 @@ define [
 				about: "<div>browser: #{platform?.name} #{platform?.version}</div>
 						<div>os: #{platform?.os?.family} #{platform?.os?.version}</div>"
 
-			Groove.createTicket params, (err, json)->
-				$scope.sent = true
+			Groove.createTicket params, (response)->
+				$scope.sending = false
+				if response.responseText == "" # Blocked request or similar
+					$scope.error = true
+				else
+					data = JSON.parse(response.responseText)
+					if data.errors?
+						$scope.error = true
+					else
+						$scope.sent = true
 				$scope.$apply()
 
 		$scope.$watch "form.subject", (newVal, oldVal) ->

+ 1 - 1
services/web/public/coffee/main/project-list/project-list.coffee

@@ -20,7 +20,7 @@ define [
 		, 10
 
 		$scope.$watch((
-			() -> $scope.projects.filter((project) -> !project.tags? or project.tags.length == 0).length
+			() -> $scope.projects.filter((project) -> (!project.tags? or project.tags.length == 0) and !project.archived).length
 		), (newVal) -> $scope.nUntagged = newVal)
 
 		storedUIOpts = JSON.parse(localStorage("project_list"))

BIN
services/web/public/img/ol-brand/logo-horizontal.png


+ 18 - 0
services/web/public/img/ol-brand/overleaf-o-grey.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 136 157" style="enable-background:new 0 0 136 157;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#9B9B9B;}
+</style>
+<g id="Page-1">
+	<g id="overleaf">
+		<g id="Group">
+			<path id="Fill-1" class="st0" d="M37.2,39.7C14.8,54,0,77.3,0,102.3C0,132.5,24.5,157,54.7,157c30.2,0,54.7-24.5,54.7-54.7
+				c0-23.3-14.6-43.3-35.2-51.1c-4-1.5-12.6-4.2-19.4-3.6c-9.8,6.2-21.8,19-27.4,31.8c8.4-10.1,21.5-14.5,33.2-12.6
+				c17.1,2.8,30.2,17.6,30.2,35.6c0,19.9-16.1,36-36,36c-11,0-20.8-4.9-27.4-12.6C17.5,114.3,15,101.9,17,89.8
+				c6.9-42.4,57.2-66.5,94.6-75.8C99.4,20.5,77.4,31.1,62,42.6c44.9,17.3,52.2-20.5,73.2-37.5C114-3.1,37.3-6.1,37.2,39.7z"/>
+		</g>
+	</g>
+</g>
+</svg>

+ 10 - 0
services/web/public/img/ol-brand/overleaf-o.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="136px" height="157px" viewBox="0 0 136 157" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="overleaf" fill="#4F9C45">
+      <g id="Group">
+        <path d="M37.205,39.652 C14.822,53.982 0,77.339 0,102.326 C0,132.522 24.48,157 54.681,157 C84.879,157 109.355,132.522 109.355,102.326 C109.355,78.986 94.729,59.05 74.151,51.215 C70.193,49.71 61.595,47.002 54.73,47.58 C44.924,53.814 32.979,66.624 27.319,79.389 C35.735,69.296 48.856,64.901 60.489,66.77 C77.615,69.547 90.697,84.408 90.697,102.321 C90.697,122.217 74.571,138.342 54.681,138.342 C43.719,138.342 33.896,133.446 27.293,125.723 C17.516,114.299 15,101.91 16.975,89.809 C23.902,47.434 74.208,23.279 111.611,14.01 C99.404,20.468 77.384,31.084 61.985,42.64 C106.909,59.981 114.169,22.123 135.202,5.181 C114.038,-3.07 37.33,-6.117 37.205,39.652 Z" id="Fill-1"></path>
+      </g>
+    </g>
+  </g>
+</svg>

BIN
services/web/public/img/teasers/track-changes/per-user-track-changes.gif


BIN
services/web/public/img/teasers/track-changes/per-user-track-changes.mp4


+ 1 - 1
services/web/public/js/ace-1.2.5/mode-latex.js

@@ -606,7 +606,7 @@ var createLatexWorker = function (session) {
     };
     worker.on("lint", function(results) {
         if(docChangePending) { docChangePending = false; };
-        hints = results.data;
+        hints = results.data.errors;
         if (hints.length > 100) {
             hints = hints.slice(0, 100); // limit to 100 errors
         };

+ 0 - 655
services/web/public/js/ace-1.2.5/mode-latex_beta.js

@@ -1,655 +0,0 @@
-ace.define("ace/mode/latex_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
-"use strict";
-
-var oop = require("../lib/oop");
-var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
-
-var LatexHighlightRules = function() {  
-
-    this.$rules = {
-        "start" : [{
-            token : "comment",
-            regex : "%.*$"
-        }, {
-            token : ["keyword", "lparen", "variable.parameter", "rparen", "lparen", "storage.type", "rparen"],
-            regex : "(\\\\(?:documentclass|usepackage|input))(?:(\\[)([^\\]]*)(\\]))?({)([^}]*)(})"
-        }, {
-            token : ["keyword","lparen", "variable.parameter", "rparen"],
-            regex : "(\\\\(?:label|v?ref|cite(?:[^{]*)))(?:({)([^}]*)(}))?"
-        }, {
-            token : ["storage.type", "lparen", "variable.parameter", "rparen"],
-            regex : "(\\\\(?:begin|end))({)(\\w*)(})"
-        }, {
-            token : "storage.type",
-            regex : "\\\\[a-zA-Z]+"
-        }, {
-            token : "lparen",
-            regex : "[[({]"
-        }, {
-            token : "rparen",
-            regex : "[\\])}]"
-        }, {
-            token : "constant.character.escape",
-            regex : "\\\\[^a-zA-Z]?"
-        }, {
-            token : "string",
-            regex : "\\${1,2}",
-            next  : "equation"
-        }],
-        "equation" : [{
-            token : "comment",
-            regex : "%.*$"
-        }, {
-            token : "string",
-            regex : "\\${1,2}",
-            next  : "start"
-        }, {
-            token : "constant.character.escape",
-            regex : "\\\\(?:[^a-zA-Z]|[a-zA-Z]+)"
-        }, {
-            token : "error", 
-            regex : "^\\s*$", 
-            next : "start" 
-        }, {
-            defaultToken : "string"
-        }]
-
-    };
-};
-oop.inherits(LatexHighlightRules, TextHighlightRules);
-
-exports.LatexHighlightRules = LatexHighlightRules;
-
-});
-
-ace.define("ace/mode/folding/latex",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode","ace/range","ace/token_iterator"], function(require, exports, module) {
-"use strict";
-
-var oop = require("../../lib/oop");
-var BaseFoldMode = require("./fold_mode").FoldMode;
-var Range = require("../../range").Range;
-var TokenIterator = require("../../token_iterator").TokenIterator;
-
-var FoldMode = exports.FoldMode = function() {};
-
-oop.inherits(FoldMode, BaseFoldMode);
-
-(function() {
-
-    this.foldingStartMarker = /^\s*\\(begin)|(section|subsection|paragraph)\b|{\s*$/;
-    this.foldingStopMarker = /^\s*\\(end)\b|^\s*}/;
-
-    this.getFoldWidgetRange = function(session, foldStyle, row) {
-        var line = session.doc.getLine(row);
-        var match = this.foldingStartMarker.exec(line);
-        if (match) {
-            if (match[1])
-                return this.latexBlock(session, row, match[0].length - 1);
-            if (match[2])
-                return this.latexSection(session, row, match[0].length - 1);
-
-            return this.openingBracketBlock(session, "{", row, match.index);
-        }
-
-        var match = this.foldingStopMarker.exec(line);
-        if (match) {
-            if (match[1])
-                return this.latexBlock(session, row, match[0].length - 1);
-
-            return this.closingBracketBlock(session, "}", row, match.index + match[0].length);
-        }
-    };
-
-    this.latexBlock = function(session, row, column) {
-        var keywords = {
-            "\\begin": 1,
-            "\\end": -1
-        };
-
-        var stream = new TokenIterator(session, row, column);
-        var token = stream.getCurrentToken();
-        if (!token || !(token.type == "storage.type" || token.type == "constant.character.escape"))
-            return;
-
-        var val = token.value;
-        var dir = keywords[val];
-
-        var getType = function() {
-            var token = stream.stepForward();
-            var type = token.type == "lparen" ?stream.stepForward().value : "";
-            if (dir === -1) {
-                stream.stepBackward();
-                if (type)
-                    stream.stepBackward();
-            }
-            return type;
-        };
-        var stack = [getType()];
-        var startColumn = dir === -1 ? stream.getCurrentTokenColumn() : session.getLine(row).length;
-        var startRow = row;
-
-        stream.step = dir === -1 ? stream.stepBackward : stream.stepForward;
-        while(token = stream.step()) {
-            if (!token || !(token.type == "storage.type" || token.type == "constant.character.escape"))
-                continue;
-            var level = keywords[token.value];
-            if (!level)
-                continue;
-            var type = getType();
-            if (level === dir)
-                stack.unshift(type);
-            else if (stack.shift() !== type || !stack.length)
-                break;
-        }
-
-        if (stack.length)
-            return;
-
-        var row = stream.getCurrentTokenRow();
-        if (dir === -1)
-            return new Range(row, session.getLine(row).length, startRow, startColumn);
-        stream.stepBackward();
-        return new Range(startRow, startColumn, row, stream.getCurrentTokenColumn());
-    };
-
-    this.latexSection = function(session, row, column) {
-        var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph"];
-
-        var stream = new TokenIterator(session, row, column);
-        var token = stream.getCurrentToken();
-        if (!token || token.type != "storage.type")
-            return;
-
-        var startLevel = keywords.indexOf(token.value);
-        var stackDepth = 0
-        var endRow = row;
-
-        while(token = stream.stepForward()) {
-            if (token.type !== "storage.type")
-                continue;
-            var level = keywords.indexOf(token.value);
-
-            if (level >= 2) {
-                if (!stackDepth)
-                    endRow = stream.getCurrentTokenRow() - 1;
-                stackDepth += level == 2 ? 1 : - 1;
-                if (stackDepth < 0)
-                    break
-            } else if (level >= startLevel)
-                break;
-        }
-
-        if (!stackDepth)
-            endRow = stream.getCurrentTokenRow() - 1;
-
-        while (endRow > row && !/\S/.test(session.getLine(endRow)))
-            endRow--;
-
-        return new Range(
-            row, session.getLine(row).length,
-            endRow, session.getLine(endRow).length
-        );
-    };
-
-}).call(FoldMode.prototype);
-
-});
-
-ace.define("ace/mode/behaviour/latex",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"], function(require, exports, module) {
-"use strict";
-
-var oop = require("../../lib/oop");
-var Behaviour = require("../behaviour").Behaviour;
-var TokenIterator = require("../../token_iterator").TokenIterator;
-var lang = require("../../lib/lang");
-
-var SAFE_INSERT_IN_TOKENS =
-    ["text", "paren.rparen", "punctuation.operator"];
-var SAFE_INSERT_BEFORE_TOKENS =
-    ["text", "paren.rparen", "punctuation.operator", "comment"];
-
-var context;
-var contextCache = {};
-var initContext = function(editor) {
-    var id = -1;
-    if (editor.multiSelect) {
-        id = editor.selection.index;
-        if (contextCache.rangeCount != editor.multiSelect.rangeCount)
-            contextCache = {rangeCount: editor.multiSelect.rangeCount};
-    }
-    if (contextCache[id])
-        return context = contextCache[id];
-    context = contextCache[id] = {
-        autoInsertedBrackets: 0,
-        autoInsertedRow: -1,
-        autoInsertedLineEnd: "",
-        maybeInsertedBrackets: 0,
-        maybeInsertedRow: -1,
-        maybeInsertedLineStart: "",
-        maybeInsertedLineEnd: ""
-    };
-};
-
-var getWrapped = function(selection, selected, opening, closing) {
-    var rowDiff = selection.end.row - selection.start.row;
-    return {
-        text: opening + selected + closing,
-        selection: [
-                0,
-                selection.start.column + 1,
-                rowDiff,
-                selection.end.column + (rowDiff ? 0 : 1)
-            ]
-    };
-};
-
-var LatexBehaviour = function() {
-    this.add("braces", "insertion", function(state, action, editor, session, text) {
-				if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) {
-					return;
-				}
-        var cursor = editor.getCursorPosition();
-        var line = session.doc.getLine(cursor.row);
-        var lastChar = line[cursor.column-1];
-        if (lastChar === '\\') {
-          return;
-        }
-        if (text == '{') {
-            initContext(editor);
-            var selection = editor.getSelectionRange();
-            var selected = session.doc.getTextRange(selection);
-            if (selected !== "" && editor.getWrapBehavioursEnabled()) {
-                return getWrapped(selection, selected, '{', '}');
-            } else if (LatexBehaviour.isSaneInsertion(editor, session)) {
-                LatexBehaviour.recordAutoInsert(editor, session, "}");
-                return {
-                    text: '{}',
-                    selection: [1, 1]
-                };
-            }
-        } else if (text == '}') {
-            initContext(editor);
-            var rightChar = line.substring(cursor.column, cursor.column + 1);
-            if (rightChar == '}') {
-                var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row});
-                if (matching !== null && LatexBehaviour.isAutoInsertedClosing(cursor, line, text)) {
-                    LatexBehaviour.popAutoInsertedClosing();
-                    return {
-                        text: '',
-                        selection: [1, 1]
-                    };
-                }
-            }
-        }
-    });
-
-    this.add("braces", "deletion", function(state, action, editor, session, range) {
-			if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) {
-				return;
-			}
-      var selected = session.doc.getTextRange(range);
-      if (!range.isMultiLine() && selected == '{') {
-        initContext(editor);
-        var line = session.doc.getLine(range.start.row);
-        var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
-        if (rightChar == '}') {
-          range.end.column++;
-          return range;
-        }
-      }
-    });
-
-    this.add("brackets", "insertion", function(state, action, editor, session, text) {
-				if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) {
-					return;
-				}
-        var cursor = editor.getCursorPosition();
-        var line = session.doc.getLine(cursor.row);
-        var lastChar = line[cursor.column-1];
-        if (lastChar === '\\') {
-          return;
-        }
-        if (text == '[') {
-            initContext(editor);
-            var selection = editor.getSelectionRange();
-            var selected = session.doc.getTextRange(selection);
-            if (selected !== "" && editor.getWrapBehavioursEnabled()) {
-                return getWrapped(selection, selected, '[', ']');
-            } else if (LatexBehaviour.isSaneInsertion(editor, session)) {
-                LatexBehaviour.recordAutoInsert(editor, session, "]");
-                return {
-                    text: '[]',
-                    selection: [1, 1]
-                };
-            }
-        } else if (text == ']') {
-            initContext(editor);
-            var rightChar = line.substring(cursor.column, cursor.column + 1);
-            if (rightChar == ']') {
-                var matching = session.$findOpeningBracket(']', {column: cursor.column + 1, row: cursor.row});
-                if (matching !== null && LatexBehaviour.isAutoInsertedClosing(cursor, line, text)) {
-                    LatexBehaviour.popAutoInsertedClosing();
-                    return {
-                        text: '',
-                        selection: [1, 1]
-                    };
-                }
-            }
-        }
-    });
-
-    this.add("brackets", "deletion", function(state, action, editor, session, range) {
-				if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) {
-					return;
-				}
-        var selected = session.doc.getTextRange(range);
-        if (!range.isMultiLine() && selected == '[') {
-            initContext(editor);
-            var line = session.doc.getLine(range.start.row);
-            var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
-            if (rightChar == ']') {
-                range.end.column++;
-                return range;
-            }
-        }
-    });
-
-    this.add("dollars", "insertion", function(state, action, editor, session, text) {
-        var cursor = editor.getCursorPosition();
-        var line = session.doc.getLine(cursor.row);
-        var lastChar = line[cursor.column-1];
-        if (lastChar === '\\') {
-          return;
-        }
-        if (text == '$') {
-            if (this.lineCommentStart && this.lineCommentStart.indexOf(text) != -1)
-                return;
-            initContext(editor);
-            var quote = text;
-            var selection = editor.getSelectionRange();
-            var selected = session.doc.getTextRange(selection);
-            if (selected !== "" && selected !== "$" && editor.getWrapBehavioursEnabled()) {
-                return getWrapped(selection, selected, quote, quote);
-            } else if (!selected) {
-                var leftChar = line.substring(cursor.column-1, cursor.column);
-                var rightChar = line.substring(cursor.column, cursor.column + 1);
-
-                var token = session.getTokenAt(cursor.row, cursor.column);
-                var rightToken = session.getTokenAt(cursor.row, cursor.column + 1);
-
-                var stringBefore = token && /string|escape/.test(token.type);
-                var stringAfter = !rightToken || /string|escape/.test(rightToken.type);
-
-                var pair;
-                if (rightChar == quote) {
-                    pair = stringBefore !== stringAfter;
-                    if (pair && /string\.end/.test(rightToken.type))
-                        pair = false;
-                } else {
-                    if (stringBefore && !stringAfter)
-                        return null; // wrap string with different quote
-                    if (stringBefore && stringAfter)
-                        return null; // do not pair quotes inside strings
-                    var wordRe = session.$mode.tokenRe;
-                    wordRe.lastIndex = 0;
-                    var isWordBefore = wordRe.test(leftChar);
-                    wordRe.lastIndex = 0;
-                    var isWordAfter = wordRe.test(leftChar);
-                    if (isWordBefore || isWordAfter)
-                        return null; // before or after alphanumeric
-                    if (rightChar && !/[\s;,.})\]\\]/.test(rightChar))
-                        return null; // there is rightChar and it isn't closing
-                    pair = true;
-                }
-                return {
-                    text: pair ? quote + quote : "",
-                    selection: [1,1]
-                };
-            }
-        }
-    });
-
-    this.add("dollars", "deletion", function(state, action, editor, session, range) {
-        var selected = session.doc.getTextRange(range);
-        if (!range.isMultiLine() && (selected == '$')) {
-            initContext(editor);
-            var line = session.doc.getLine(range.start.row);
-            var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
-            if (rightChar == selected) {
-                range.end.column++;
-                return range;
-            }
-        }
-    });
-
-};
-
-
-LatexBehaviour.isSaneInsertion = function(editor, session) {
-    var cursor = editor.getCursorPosition();
-    var iterator = new TokenIterator(session, cursor.row, cursor.column);
-    if (!this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) {
-        var iterator2 = new TokenIterator(session, cursor.row, cursor.column + 1);
-        if (!this.$matchTokenType(iterator2.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS))
-            return false;
-    }
-    iterator.stepForward();
-    return iterator.getCurrentTokenRow() !== cursor.row ||
-        this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_BEFORE_TOKENS);
-};
-
-LatexBehaviour.$matchTokenType = function(token, types) {
-    return types.indexOf(token.type || token) > -1;
-};
-
-LatexBehaviour.recordAutoInsert = function(editor, session, bracket) {
-    var cursor = editor.getCursorPosition();
-    var line = session.doc.getLine(cursor.row);
-    if (!this.isAutoInsertedClosing(cursor, line, context.autoInsertedLineEnd[0]))
-        context.autoInsertedBrackets = 0;
-    context.autoInsertedRow = cursor.row;
-    context.autoInsertedLineEnd = bracket + line.substr(cursor.column);
-    context.autoInsertedBrackets++;
-};
-
-LatexBehaviour.isAutoInsertedClosing = function(cursor, line, bracket) {
-    return context.autoInsertedBrackets > 0 &&
-        cursor.row === context.autoInsertedRow &&
-        bracket === context.autoInsertedLineEnd[0] &&
-        line.substr(cursor.column) === context.autoInsertedLineEnd;
-};
-
-LatexBehaviour.popAutoInsertedClosing = function() {
-    context.autoInsertedLineEnd = context.autoInsertedLineEnd.substr(1);
-    context.autoInsertedBrackets--;
-};
-
-
-oop.inherits(LatexBehaviour, Behaviour);
-
-exports.LatexBehaviour = LatexBehaviour;
-});
-
-ace.define("ace/mode/latex_beta",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/latex_highlight_rules","ace/mode/folding/latex","ace/range","ace/worker/worker_client","ace/mode/behaviour/latex"], function(require, exports, module) {
-"use strict";
-
-var oop = require("../lib/oop");
-var TextMode = require("./text").Mode;
-var LatexHighlightRules = require("./latex_highlight_rules").LatexHighlightRules;
-var LatexFoldMode = require("./folding/latex").FoldMode;
-var Range = require("../range").Range;
-var WorkerClient = require("ace/worker/worker_client").WorkerClient;
-var LatexBehaviour = require("./behaviour/latex").LatexBehaviour;
-
-var createLatexWorker = function (session) {
-    var doc = session.getDocument();
-    var selection = session.getSelection();
-    var cursorAnchor = selection.lead;
-
-    var savedRange = {};
-    var suppressions = [];
-    var hints = [];
-    var changeHandler = null;
-    var docChangePending = false;
-    var firstPass = true;
-
-    var worker = new WorkerClient(["ace"], "ace/mode/latex_beta_worker", "LatexWorker");
-    worker.attachToDocument(doc);
-    var docChangeHandler = doc.on("change", function () {
-        docChangePending = true;
-        if(changeHandler) {
-            clearTimeout(changeHandler);
-            changeHandler = null;
-        }
-    });
-
-    var cursorHandler = selection.on("changeCursor", function () {
-        if (docChangePending) { return; } ;
-        changeHandler = setTimeout(function () {
-            updateMarkers({cursorMoveOnly:true});
-            suppressions = [];
-            changeHandler = null;
-        }, 100);
-    });
-
-    var updateMarkers = function (options) {
-        if (!options) { options = {};};
-        var cursorMoveOnly = options.cursorMoveOnly;
-        var annotations = [];
-        var newRange = {};
-        var cursor = selection.getCursor();
-        var maxRow = session.getLength() - 1;
-        var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0;
-        var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol);
-
-        suppressions = [];
-
-        for (var i = 0, len = hints.length; i<len; i++) {
-            var hint = hints[i];
-
-            var suppressedChanges = 0;
-            var hintRange = new Range(hint.start_row, hint.start_col, hint.end_row, hint.end_col);
-
-            var cursorInRange = hintRange.insideEnd(cursor.row, cursor.column);
-            var cursorAtStart = hintRange.isStart(cursor.row, cursor.column - 1); // cursor after start not before
-            var cursorAtEnd = hintRange.isEnd(cursor.row, cursor.column);
-            if (hint.suppressIfEditing && (cursorAtStart || cursorAtEnd)) {
-                suppressions.push(hintRange);
-                if (!hint.suppressed) { suppressedChanges++; };
-                hint.suppressed = true;
-                continue;
-            }
-            var isCascadeError = false;
-            for (var j = 0, suplen = suppressions.length; j < suplen; j++) {
-                var badRange = suppressions[j];
-                if (badRange.intersects(hintRange)) {
-                    isCascadeError = true;
-                    break;
-                }
-            }
-            if(isCascadeError) {
-                if (!hint.suppressed) { suppressedChanges++; };
-                hint.suppressed = true;
-                continue;
-            };
-
-            if (hint.suppressed) { suppressedChanges++; };
-            hint.suppressed = false;
-
-            annotations.push(hint);
-            if (hint.type === "info") {
-                continue;
-            };
-            var key = hintRange.toString() + (cursorInRange ? "+cursor" : "");
-            newRange[key] = {hint: hint, cursorInRange: cursorInRange, range: hintRange};
-        }
-        for (key in newRange) {
-            if (!savedRange[key]) {  // doesn't exist in already displayed errors
-                var new_range = newRange[key].range;
-                cursorInRange = newRange[key].cursorInRange;
-                hint = newRange[key].hint;
-                var errorAtStart = (hint.row === hint.start_row && hint.column === hint.start_col);
-                var movableStart = (cursorInRange && !errorAtStart) && !cursorAtEndOfDocument;
-                var movableEnd = (cursorInRange && errorAtStart) && !cursorAtEndOfDocument;
-                var a = movableStart ? cursorAnchor : doc.createAnchor(new_range.start);
-                var b = movableEnd ? cursorAnchor : doc.createAnchor(new_range.end);
-                var range = new Range();
-                range.start = a;
-                range.end = b;
-                var cssClass = "ace_error-marker";
-                if (hint.type === "warning") { cssClass = "ace_highlight-marker"; };
-                range.id = session.addMarker(range, cssClass, "text");
-                savedRange[key] = range;
-            }
-        }
-        for (key in savedRange) {
-            if (!newRange[key]) {  // no longer present in list of errors to display
-                range = savedRange[key];
-                if (range.start !== cursorAnchor) { range.start.detach(); }
-                if (range.end !== cursorAnchor) { range.end.detach(); }
-                session.removeMarker(range.id);
-                delete savedRange[key];
-            }
-        }
-        if (!cursorMoveOnly || suppressedChanges) {
-            if (firstPass) {
-                if (annotations.length > 0) {
-                    var originalAnnotations = session.getAnnotations();
-                    session.setAnnotations(originalAnnotations.concat(annotations));
-                };
-                firstPass = false;
-            } else {
-                session.setAnnotations(annotations);
-            }
-        };
-
-    };
-    worker.on("lint", function(results) {
-        if(docChangePending) { docChangePending = false; };
-        hints = results.data;
-        if (hints.length > 100) {
-            hints = hints.slice(0, 100); // limit to 100 errors
-        };
-        updateMarkers();
-    });
-    worker.on("terminate", function() {
-        if(changeHandler) {
-            clearTimeout(changeHandler);
-            changeHandler = null;
-        }
-        doc.off("change", docChangeHandler);
-        selection.off("changeCursor", cursorHandler);
-        for (var key in savedRange) {
-            var range = savedRange[key];
-            if (range.start !== cursorAnchor) { range.start.detach(); }
-            if (range.end !== cursorAnchor) { range.end.detach(); }
-            session.removeMarker(range.id);
-        }
-        savedRange = {};
-        hints = [];
-        suppressions = [];
-        session.clearAnnotations();
-    });
-
-    return worker;
-};
-
-var Mode = function() {
-    this.HighlightRules = LatexHighlightRules;
-    this.foldingRules = new LatexFoldMode();
-    this.$behaviour = new LatexBehaviour();
-    this.createWorker = createLatexWorker;
-};
-oop.inherits(Mode, TextMode);
-
-(function() {
-    this.type = "text";
-
-    this.lineCommentStart = "%";
-
-    this.$id = "ace/mode/latex_beta";
-}).call(Mode.prototype);
-
-exports.Mode = Mode;
-
-});

+ 291 - 189
services/web/public/js/ace-1.2.5/worker-latex.js

@@ -1419,6 +1419,34 @@ var LatexWorker = exports.LatexWorker = function(sender) {
 
 oop.inherits(LatexWorker, Mirror);
 
+(function() {
+    var disabled = false;
+    this.onUpdate = function() {
+        if (disabled) { return ; };
+
+        var value = this.doc.getValue();
+        var errors = [];
+        var contexts = [];
+        try {
+            if (value) {
+                var result = Parse(value);
+                errors = result.errors;
+                contexts = result.contexts;
+            }
+        } catch (e) {
+            console.log(e);
+            disabled = true;
+            this.sender.emit("fatal-error", e);
+            errors = [];
+        }
+        this.sender.emit("lint", {
+          errors: errors,
+          contexts: contexts
+        });
+    };
+
+}).call(LatexWorker.prototype);
+
 var Tokenise = function (text) {
     var Tokens = [];
     var Comments = [];
@@ -1503,22 +1531,8 @@ var Tokenise = function (text) {
                 }
                 idx = SPECIAL.lastIndex = nextSpecialPos;
             }
-        } else if (code === "{") {  // open group
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "}") {  // close group
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "$") {  // math mode
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "&") {  // tabalign
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "#") {  // macro parameter
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "^") {  // superscript
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "_") {  // subscript
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "~") {  // active character (space)
-            Tokens.push([lineNumber, code, pos]);
+        } else if (["{", "}", "$", "&", "#", "^", "_", "~"].indexOf(code) > -1) {  // special characters
+            Tokens.push([lineNumber, code, pos, pos+1]);
         } else {
             throw "unrecognised character " + code;
         }
@@ -1539,15 +1553,15 @@ var read1arg = function (TokeniseResult, k, options) {
     };
 
     var open = Tokens[k+1];
-    var env = Tokens[k+2];
+    var delimiter = Tokens[k+2];
     var close = Tokens[k+3];
-    var envName;
+    var delimiterName;
 
     if(open && open[1] === "\\") {
-        envName = open[4]; // array element 4 is command sequence
+        delimiterName = open[4]; // array element 4 is command sequence
         return k + 1;
-    } else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") {
-        envName = env[4]; // NOTE: if we were actually using this, keep track of * above
+    } else if(open && open[1] === "{" && delimiter && delimiter[1] === "\\" && close && close[1] === "}") {
+        delimiterName = delimiter[4]; // NOTE: if we were actually using this, keep track of * above
         return k + 3; // array element 4 is command sequence
     } else {
         return null;
@@ -1579,21 +1593,21 @@ var read1name = function (TokeniseResult, k) {
     var text = TokeniseResult.text;
 
     var open = Tokens[k+1];
-    var env = Tokens[k+2];
+    var delimiter = Tokens[k+2];
     var close = Tokens[k+3];
 
-    if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
-        var envName = text.substring(env[2], env[3]);
+    if(open && open[1] === "{" && delimiter && delimiter[1] === "Text" && close && close[1] === "}") {
+        var delimiterName = text.substring(delimiter[2], delimiter[3]);
         return k + 3;
-    } else if (open && open[1] === "{" && env && env[1] === "Text") {
-        envName = "";
+    } else if (open && open[1] === "{" && delimiter && delimiter[1] === "Text") {
+        delimiterName = "";
         for (var j = k + 2, tok; (tok = Tokens[j]); j++) {
             if (tok[1] === "Text") {
                 var str = text.substring(tok[2], tok[3]);
                 if (!str.match(/^\S*$/)) { break; }
-                envName = envName + str;
+                delimiterName = delimiterName + str;
             } else if (tok[1] === "_") {
-                envName = envName + "_";
+                delimiterName = delimiterName + "_";
             } else {
                 break;
             }
@@ -1644,6 +1658,7 @@ var readOptionalParams = function(TokeniseResult, k) {
     };
     var count = 0;
     var nextToken = Tokens[k+1];
+    if (!nextToken) { return null };
     var pos = nextToken[2];
 
     for (var i = pos, end = text.length; i < end; i++) {
@@ -1788,7 +1803,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
 
     var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
     var TokenError = ErrorReporter.TokenError;
-    var Environments = new EnvHandler(ErrorReporter);
+    var Environments = new EnvHandler(TokeniseResult, ErrorReporter);
 
     var nextGroupMathMode = null; // if the next group should have
     var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes
@@ -1815,28 +1830,28 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
         if (type === "\\") {
             if (seq === "begin" || seq === "end") {
                 var open = Tokens[i+1];
-                var env = Tokens[i+2];
+                var delimiter = Tokens[i+2];
                 var close = Tokens[i+3];
-                if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
-                    var envName = text.substring(env[2], env[3]);
-                    Environments.push({command: seq, name: envName, token: token, closeToken: close});
+                if(open && open[1] === "{" && delimiter && delimiter[1] === "Text" && close && close[1] === "}") {
+                    var delimiterName = text.substring(delimiter[2], delimiter[3]);
+                    Environments.push({command: seq, name: delimiterName, token: token, closeToken: close});
                     i = i + 3; // advance past these tokens
                 } else {
-                    if (open && open[1] === "{" && env && env[1] === "Text") {
-                        envName = "";
+                    if (open && open[1] === "{" && delimiter && delimiter[1] === "Text") {
+                        delimiterName = "";
                         for (var j = i + 2, tok; (tok = Tokens[j]); j++) {
                             if (tok[1] === "Text") {
                                 var str = text.substring(tok[2], tok[3]);
                                 if (!str.match(/^\S*$/)) { break; }
-                                envName = envName + str;
+                                delimiterName = delimiterName + str;
                             } else if (tok[1] === "_") {
-                                envName = envName + "_";
+                                delimiterName = delimiterName + "_";
                             } else {
                                 break;
                             }
                         }
                         if (tok && tok[1] === "}") {
-                            Environments.push({command: seq, name: envName, token: token, closeToken: close});
+                            Environments.push({command: seq, name: delimiterName, token: token, closeToken: close});
                             i = j; // advance past these tokens
                             continue;
                         }
@@ -1844,8 +1859,8 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                     var endToken = null;
                     if (open && open[1] === "{") {
                         endToken = open; // we've got a {
-                        if (env && env[1] === "Text") {
-                            endToken = env.slice(); // we've got some text following the {
+                        if (delimiter && delimiter[1] === "Text") {
+                            endToken = delimiter.slice(); // we've got some text following the {
                             start = endToken[2]; end = endToken[3];
                             for (j = start; j < end; j++) {
                                 var char = text[j];
@@ -1978,7 +1993,12 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             var nextIsDollar = lookAhead && lookAhead[1] === "$";
             currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
             if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) {
-                Environments.push({command:"$$", token:token});
+                if (currentMathMode && currentMathMode.command == "$$") {
+                    var delimiterToken = lookAhead;
+                } else {
+                    var delimiterToken = token;
+                }
+                Environments.push({command:"$$", token:delimiterToken});
                 i = i + 1;
             } else {
                 Environments.push({command:"$", token:token});
@@ -1999,74 +2019,174 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
     return Environments;
 };
 
-var EnvHandler = function (ErrorReporter) {
+var DocumentTree = function(TokeniseResult) {
+    var tree = {
+      children: []
+    };
+    var stack = [tree];
+    
+    this.openEnv = function(startDelimiter) {
+        var currentNode = this.getCurrentNode();
+        var newNode = {
+            startDelimiter: startDelimiter,
+            children: []
+        };
+        currentNode.children.push(newNode);
+        stack.push(newNode);
+    };
+    
+    this.closeEnv = function(endDelimiter) {
+        if (stack.length == 1) {
+            return null
+        }
+        var currentNode = stack.pop();
+        currentNode.endDelimiter = endDelimiter;
+        return currentNode.startDelimiter;
+    };
+    
+    this.getNthPreviousNode = function(n) {
+        var offset = stack.length - n - 1;
+        if (offset < 0)
+            return null;
+        return stack[offset];
+    }
+    
+    this.getCurrentNode = function() {
+        return this.getNthPreviousNode(0);
+    }
+    
+    this.getCurrentDelimiter = function() {
+        return this.getCurrentNode().startDelimiter;
+    };
+
+    this.getPreviousDelimiter = function() {
+        var node = this.getNthPreviousNode(1);
+        if (!node)
+            return null
+        return node.startDelimiter;
+    }
+    
+    this.getDepth = function() {
+        return (stack.length - 1) // Root node doesn't count
+    }
+    
+    this.getContexts = function() {
+        var linePosition = TokeniseResult.linePosition;
+
+        function tokenToRange(token) {
+            var line = token[0], start = token[2], end = token[3];
+            var start_col = start - linePosition[line];
+            if (!end) { end = start + 1; } ;
+            var end_col = end - linePosition[line];
+            return {
+                start: {
+                    row: line,
+                    column: start_col
+                },
+                end: {
+                    row: line,
+                    column: end_col
+                }
+            }
+        };
+        
+        function getContextsFromNode(node) {
+            if (node.startDelimiter && node.startDelimiter.mathMode) {
+                var context = {
+                    type: "math",
+                    range: {
+                        start: tokenToRange(node.startDelimiter.token).start
+                    }
+                };
+                if (node.endDelimiter) {
+                    var closeToken = node.endDelimiter.closeToken || node.endDelimiter.token;
+                    context.range.end = tokenToRange(closeToken).end;
+                };
+                return [context];
+            } else {
+                var contexts = [];
+                for (var i = 0; i < node.children.length; i++) {
+                    var child = node.children[i];
+                    contexts = contexts.concat(getContextsFromNode(child));
+                }
+                return contexts;
+            }
+        };
+        
+        return getContextsFromNode(tree);
+    }
+}
+
+var EnvHandler = function (TokeniseResult, ErrorReporter) {
     var ErrorTo = ErrorReporter.EnvErrorTo;
     var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
     var ErrorFrom = ErrorReporter.EnvErrorFrom;
 
-    var envs = [];
+    var delimiters = [];
 
-    var state = [];
+    var document = new DocumentTree(TokeniseResult);
     var documentClosed = null;
     var inVerbatim = false;
     var verbatimRanges = [];
+    
+    this.getDocument = function() {
+        return document;
+    };
 
-    this.Environments = envs;
-
-    this.push = function (newEnv) {
-        this.setEnvProps(newEnv);
-        this.checkAndUpdateState(newEnv);
-        envs.push(newEnv);
+    this.push = function (newDelimiter) {
+        this.setDelimiterProps(newDelimiter);
+        this.checkAndUpdateState(newDelimiter);
+        delimiters.push(newDelimiter);
     };
 
-    this._endVerbatim = function (thisEnv) {
-        var lastEnv = state.pop();
-        if (lastEnv && lastEnv.name === thisEnv.name) {
+    this._endVerbatim = function (thisDelimiter) {
+        var lastDelimiter = document.getCurrentDelimiter();
+        if (lastDelimiter && lastDelimiter.name === thisDelimiter.name) {
             inVerbatim = false;
-            verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
-        } else {
-            if(lastEnv) { state.push(lastEnv); } ;
+            document.closeEnv(thisDelimiter);
+            verbatimRanges.push({start: lastDelimiter.token[2], end: thisDelimiter.token[2]});
         }
     };
 
     var invalidEnvs = [];
 
-    this._end = function (thisEnv) {
+    this._end = function (thisDelimiter) {
         do {
-            var lastEnv = state.pop();
+            var lastDelimiter = document.getCurrentDelimiter();
             var retry = false;
             var i;
 
-            if (closedBy(lastEnv, thisEnv)) {
-                if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
-                    documentClosed = thisEnv;
+            if (closedBy(lastDelimiter, thisDelimiter)) {
+                document.closeEnv(thisDelimiter);
+                if (thisDelimiter.command === "end" && thisDelimiter.name === "document" && !documentClosed) {
+                    documentClosed = thisDelimiter;
                 };
                 return;
-            } else if (!lastEnv) {
+            } else if (!lastDelimiter) {
                 if (documentClosed) {
-                    ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+                    ErrorFromTo(documentClosed, thisDelimiter, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
                 } else {
-                    ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
+                    ErrorTo(thisDelimiter, "unexpected " + getName(thisDelimiter));
                 }
-            } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
+            } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisDelimiter) > -1)) {
                 invalidEnvs.splice(i, 1);
-                if (lastEnv) { state.push(lastEnv); } ;
                 return;
             } else {
-                var status = reportError(lastEnv, thisEnv);
-                if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) {
-                    invalidEnvs.push(lastEnv);
+                var status = reportError(lastDelimiter, thisDelimiter);
+                if (delimiterPrecedence(lastDelimiter) < delimiterPrecedence(thisDelimiter)) {
+                    document.closeEnv();
+                    invalidEnvs.push(lastDelimiter);
                     retry = true;
                 } else {
-                    var prevLastEnv = state.pop();
-                    if(prevLastEnv) {
-                        if (thisEnv.name === prevLastEnv.name) {
+                    var prevDelimiter = document.getPreviousDelimiter();
+                    if(prevDelimiter) {
+                        if (thisDelimiter.name === prevDelimiter.name) {
+                            document.closeEnv() // Close current env
+                            document.closeEnv(thisDelimiter) // Close previous env
                             return;
-                        } else {
-                            state.push(prevLastEnv);
                         }
                     }
-                    invalidEnvs.push(lastEnv);
+                    invalidEnvs.push(lastDelimiter);
                 }
 
             }
@@ -2082,28 +2202,28 @@ var EnvHandler = function (ErrorReporter) {
         "$$": "$$"
     };
 
-    var closedBy = function (lastEnv, thisEnv) {
-        if (!lastEnv) {
+    var closedBy = function (lastDelimiter, thisDelimiter) {
+        if (!lastDelimiter) {
             return false ;
-        } else if (thisEnv.command === "end") {
-            return lastEnv.command === "begin" && lastEnv.name === thisEnv.name;
-        } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) {
+        } else if (thisDelimiter.command === "end") {
+            return lastDelimiter.command === "begin" && lastDelimiter.name === thisDelimiter.name;
+        } else if (thisDelimiter.command === CLOSING_DELIMITER[lastDelimiter.command]) {
             return true;
         } else {
             return false;
         }
     };
 
-    var indexOfClosingEnvInArray = function (envs, thisEnv) {
-        for (var i = 0, n = envs.length; i < n ; i++) {
-            if (closedBy(envs[i], thisEnv)) {
+    var indexOfClosingEnvInArray = function (delimiters, thisDelimiter) {
+        for (var i = 0, n = delimiters.length; i < n ; i++) {
+            if (closedBy(delimiters[i], thisDelimiter)) {
                 return i;
             }
         }
         return -1;
     };
 
-    var envPrecedence = function (env) {
+    var delimiterPrecedence = function (delimiter) {
         var openScore = {
             "{" : 1,
             "left" : 2,
@@ -2118,14 +2238,14 @@ var EnvHandler = function (ErrorReporter) {
             "$$" : 5,
             "end": 4
         };
-        if (env.command) {
-            return openScore[env.command] || closeScore[env.command];
+        if (delimiter.command) {
+            return openScore[delimiter.command] || closeScore[delimiter.command];
         } else {
             return 0;
         }
     };
 
-    var getName = function(env) {
+    var getName = function(delimiter) {
         var description = {
             "{" : "open group {",
             "}" : "close group }",
@@ -2138,12 +2258,12 @@ var EnvHandler = function (ErrorReporter) {
             "left" : "\\left",
             "right" : "\\right"
         };
-        if (env.command === "begin" || env.command === "end") {
-            return "\\" + env.command + "{" + env.name + "}";
-        } else if (env.command in description) {
-            return description[env.command];
+        if (delimiter.command === "begin" || delimiter.command === "end") {
+            return "\\" + delimiter.command + "{" + delimiter.name + "}";
+        } else if (delimiter.command in description) {
+            return description[delimiter.command];
         } else {
-            return env.command;
+            return delimiter.command;
         }
     };
 
@@ -2151,81 +2271,81 @@ var EnvHandler = function (ErrorReporter) {
     var UNCLOSED_GROUP = 2;
     var UNCLOSED_ENV = 3;
 
-    var reportError = function(lastEnv, thisEnv) {
-        if (!lastEnv) { // unexpected close, nothing was open!
+    var reportError = function(lastDelimiter, thisDelimiter) {
+        if (!lastDelimiter) { // unexpected close, nothing was open!
             if (documentClosed) {
-                ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
+                ErrorFromTo(documentClosed, thisDelimiter, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
             } else {
-                ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
+                ErrorTo(thisDelimiter, "unexpected " + getName(thisDelimiter));
             };
             return EXTRA_CLOSE;
-        } else if (lastEnv.command === "{" && thisEnv.command === "end") {
-            ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
+        } else if (lastDelimiter.command === "{" && thisDelimiter.command === "end") {
+            ErrorFromTo(lastDelimiter, thisDelimiter, "unclosed " + getName(lastDelimiter) + " found at " + getName(thisDelimiter),
                         {suppressIfEditing:true, errorAtStart: true, type:"warning"});
             return UNCLOSED_GROUP;
         } else {
-            var pLast = envPrecedence(lastEnv);
-            var pThis = envPrecedence(thisEnv);
+            var pLast = delimiterPrecedence(lastDelimiter);
+            var pThis = delimiterPrecedence(thisDelimiter);
             if (pThis > pLast) {
-                ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
+                ErrorFromTo(lastDelimiter, thisDelimiter, "unclosed " + getName(lastDelimiter) + " found at " + getName(thisDelimiter),
                            {suppressIfEditing:true, errorAtStart: true});
             } else {
-                ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv));
+                ErrorFromTo(lastDelimiter, thisDelimiter, "unexpected " + getName(thisDelimiter) + " after " + getName(lastDelimiter));
             }
             return UNCLOSED_ENV;
         };
     };
 
-    this._beginMathMode = function (thisEnv) {
+    this._beginMathMode = function (thisDelimiter) {
         var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
         if (currentMathMode) {
-            ErrorFrom(thisEnv, getName(thisEnv) + " used inside existing math mode " + getName(currentMathMode),
+            ErrorFrom(thisDelimiter, getName(thisDelimiter) + " used inside existing math mode " + getName(currentMathMode),
                       {suppressIfEditing:true, errorAtStart: true, mathMode:true});
         };
-        thisEnv.mathMode = thisEnv;
-        state.push(thisEnv);
+        thisDelimiter.mathMode = thisDelimiter;
+        document.openEnv(thisDelimiter);
     };
 
-    this._toggleMathMode = function (thisEnv) {
-        var lastEnv = state.pop();
-        if (closedBy(lastEnv, thisEnv)) {
+    this._toggleMathMode = function (thisDelimiter) {
+        var lastDelimiter = document.getCurrentDelimiter();
+        if (closedBy(lastDelimiter, thisDelimiter)) {
+            document.closeEnv(thisDelimiter)
             return;
         } else {
-            if (lastEnv) {state.push(lastEnv);}
-            if (lastEnv && lastEnv.mathMode) {
-                this._end(thisEnv);
+            if (lastDelimiter && lastDelimiter.mathMode) {
+                this._end(thisDelimiter);
             } else {
-                thisEnv.mathMode = thisEnv;
-                state.push(thisEnv);
+                thisDelimiter.mathMode = thisDelimiter;
+                document.openEnv(thisDelimiter);
             }
         };
     };
 
     this.getMathMode = function () {
-        var n = state.length;
-        if (n > 0) {
-            return state[n-1].mathMode;
+        var currentDelimiter = document.getCurrentDelimiter();
+        if (currentDelimiter) {
+            return currentDelimiter.mathMode;
         } else {
             return null;
         }
     };
 
     this.insideGroup = function () {
-        var n = state.length;
-        if (n > 0) {
-            return (state[n-1].command === "{");
+        var currentDelimiter = document.getCurrentDelimiter();
+        if (currentDelimiter) {
+            return (currentDelimiter.command === "{");
         } else {
             return null;
         }
     };
 
     var resetMathMode = function () {
-        var n = state.length;
-        if (n > 0) {
-            var lastMathMode = state[n-1].mathMode;
+        var currentDelimiter = document.getCurrentDelimiter();
+        if (currentDelimiter) {
+            var lastMathMode = currentDelimiter.mathMode;
             do {
-                var lastEnv = state.pop();
-            } while (lastEnv && lastEnv !== lastMathMode);
+                var lastDelimiter = document.closeEnv();
+            } while (lastDelimiter && lastDelimiter !== lastMathMode);
         } else {
             return;
         }
@@ -2233,42 +2353,42 @@ var EnvHandler = function (ErrorReporter) {
 
     this.resetMathMode = resetMathMode;
 
-    var getNewMathMode = function (currentMathMode, thisEnv) {
+    var getNewMathMode = function (currentMathMode, thisDelimiter) {
         var newMathMode = null;
 
-        if (thisEnv.command === "{") {
-            if (thisEnv.mathMode !== null) {
-                newMathMode = thisEnv.mathMode;
+        if (thisDelimiter.command === "{") {
+            if (thisDelimiter.mathMode !== null) {
+                newMathMode = thisDelimiter.mathMode;
             } else {
                 newMathMode = currentMathMode;
             }
-        } else if (thisEnv.command === "left") {
+        } else if (thisDelimiter.command === "left") {
             if (currentMathMode === null) {
-                ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true});
+                ErrorFrom(thisDelimiter, "\\left can only be used in math mode", {mathMode: true});
             };
             newMathMode = currentMathMode;
-        } else if (thisEnv.command === "begin") {
-            var name = thisEnv.name;
+        } else if (thisDelimiter.command === "begin") {
+            var name = thisDelimiter.name;
             if (name) {
                 if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
                     if (currentMathMode) {
-                        ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
+                        ErrorFromTo(currentMathMode, thisDelimiter, thisDelimiter.name + " used inside " + getName(currentMathMode),
                                     {suppressIfEditing:true, errorAtStart: true, mathMode: true});
                         resetMathMode();
                     };
                     newMathMode = null;
                 } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) {
                     if (currentMathMode === null) {
-                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true});
+                        ErrorFrom(thisDelimiter, thisDelimiter.name + " not inside math mode", {mathMode: true});
                     };
                     newMathMode = currentMathMode;
                 } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
                     if (currentMathMode) {
-                        ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
+                        ErrorFromTo(currentMathMode, thisDelimiter, thisDelimiter.name + " used inside " + getName(currentMathMode),
                                     {suppressIfEditing:true, errorAtStart: true, mathMode: true});
                         resetMathMode();
                     };
-                    newMathMode = thisEnv;
+                    newMathMode = thisDelimiter;
                 } else {
                     newMathMode = undefined;  // undefined means we don't know if we are in math mode or not
                 }
@@ -2277,41 +2397,41 @@ var EnvHandler = function (ErrorReporter) {
         return newMathMode;
     };
 
-    this.checkAndUpdateState = function (thisEnv) {
+    this.checkAndUpdateState = function (thisDelimiter) {
         if (inVerbatim) {
-            if (thisEnv.command === "end") {
-                this._endVerbatim(thisEnv);
+            if (thisDelimiter.command === "end") {
+                this._endVerbatim(thisDelimiter);
             } else {
                 return; // ignore anything in verbatim environments
             }
-        } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") {
-            if (thisEnv.verbatim) {inVerbatim = true;};
+        } else if(thisDelimiter.command === "begin" || thisDelimiter.command === "{" || thisDelimiter.command === "left") {
+            if (thisDelimiter.verbatim) {inVerbatim = true;};
             var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
-            var newMathMode = getNewMathMode(currentMathMode, thisEnv);
-            thisEnv.mathMode = newMathMode;
-            state.push(thisEnv);
-        } else if (thisEnv.command === "end") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "(" || thisEnv.command === "[") {
-            this._beginMathMode(thisEnv);
-        } else if (thisEnv.command === ")" || thisEnv.command === "]") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "}") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "right") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "$" || thisEnv.command === "$$") {
-            this._toggleMathMode(thisEnv);
+            var newMathMode = getNewMathMode(currentMathMode, thisDelimiter);
+            thisDelimiter.mathMode = newMathMode;
+            document.openEnv(thisDelimiter);
+        } else if (thisDelimiter.command === "end") {
+            this._end(thisDelimiter);
+        } else if (thisDelimiter.command === "(" || thisDelimiter.command === "[") {
+            this._beginMathMode(thisDelimiter);
+        } else if (thisDelimiter.command === ")" || thisDelimiter.command === "]") {
+            this._end(thisDelimiter);
+        } else if (thisDelimiter.command === "}") {
+            this._end(thisDelimiter);
+        } else if (thisDelimiter.command === "right") {
+            this._end(thisDelimiter);
+        } else if (thisDelimiter.command === "$" || thisDelimiter.command === "$$") {
+            this._toggleMathMode(thisDelimiter);
         }
     };
 
     this.close = function () {
-        while (state.length > 0) {
-            var thisEnv = state.pop();
-            if (thisEnv.command === "{") {
-                ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
+        while (document.getDepth() > 0) {
+            var thisDelimiter = document.closeEnv();
+            if (thisDelimiter.command === "{") {
+                ErrorFrom(thisDelimiter, "unclosed group {", {type:"warning"});
             } else {
-                ErrorFrom(thisEnv, "unclosed " + getName(thisEnv));
+                ErrorFrom(thisDelimiter, "unclosed " + getName(thisDelimiter));
             }
         }
         var vlen = verbatimRanges.length;
@@ -2331,10 +2451,10 @@ var EnvHandler = function (ErrorReporter) {
         }
     };
 
-    this.setEnvProps = function (env) {
-        var name = env.name ;
+    this.setDelimiterProps = function (delimiter) {
+        var name = delimiter.name ;
         if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) {
-            env.verbatim = true;
+            delimiter.verbatim = true;
         }
     };
 };
@@ -2458,9 +2578,9 @@ var ErrorReporter = function (TokeniseResult) {
         errors.push(err);
     };
 
-    this.EnvErrorFrom = function (env, message, options) {
+    this.EnvErrorFrom = function (delimiter, message, options) {
         if(!options) { options = {} ; };
-        var token = env.token;
+        var token = delimiter.token;
         var line = token[0], type = token[1], start = token[2], end = token[3];
         var start_col = start - linePosition[line];
         var end_col = Infinity;
@@ -2481,29 +2601,11 @@ var Parse = function (text) {
     var Reporter = new ErrorReporter(TokeniseResult);
     var Environments = InterpretTokens(TokeniseResult, Reporter);
     Environments.close();
-    return Reporter.getErrors();
+    return {
+      errors: Reporter.getErrors(),
+      contexts: Environments.getDocument().getContexts()
+    }
 };
-
-(function() {
-    var disabled = false;
-
-    this.onUpdate = function() {
-        if (disabled) { return ; };
-
-        var value = this.doc.getValue();
-        var errors = [];
-        try {
-            if (value)
-                errors = Parse(value);
-        } catch (e) {
-            disabled = true;
-            errors = [];
-        }
-        this.sender.emit("lint", errors);
-    };
-
-}).call(LatexWorker.prototype);
-
 });
 
 ace.define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) {

+ 0 - 3207
services/web/public/js/ace-1.2.5/worker-latex_beta.js

@@ -1,3207 +0,0 @@
-"no use strict";
-;(function(window) {
-if (typeof window.window != "undefined" && window.document)
-    return;
-if (window.require && window.define)
-    return;
-
-if (!window.console) {
-    window.console = function() {
-        var msgs = Array.prototype.slice.call(arguments, 0);
-        postMessage({type: "log", data: msgs});
-    };
-    window.console.error =
-    window.console.warn = 
-    window.console.log =
-    window.console.trace = window.console;
-}
-window.window = window;
-window.ace = window;
-
-window.onerror = function(message, file, line, col, err) {
-    postMessage({type: "error", data: {
-        message: message,
-        data: err.data,
-        file: file,
-        line: line, 
-        col: col,
-        stack: err.stack
-    }});
-};
-
-window.normalizeModule = function(parentId, moduleName) {
-    // normalize plugin requires
-    if (moduleName.indexOf("!") !== -1) {
-        var chunks = moduleName.split("!");
-        return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]);
-    }
-    // normalize relative requires
-    if (moduleName.charAt(0) == ".") {
-        var base = parentId.split("/").slice(0, -1).join("/");
-        moduleName = (base ? base + "/" : "") + moduleName;
-        
-        while (moduleName.indexOf(".") !== -1 && previous != moduleName) {
-            var previous = moduleName;
-            moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "");
-        }
-    }
-    
-    return moduleName;
-};
-
-window.require = function require(parentId, id) {
-    if (!id) {
-        id = parentId;
-        parentId = null;
-    }
-    if (!id.charAt)
-        throw new Error("worker.js require() accepts only (parentId, id) as arguments");
-
-    id = window.normalizeModule(parentId, id);
-
-    var module = window.require.modules[id];
-    if (module) {
-        if (!module.initialized) {
-            module.initialized = true;
-            module.exports = module.factory().exports;
-        }
-        return module.exports;
-    }
-   
-    if (!window.require.tlns)
-        return console.log("unable to load " + id);
-    
-    var path = resolveModuleId(id, window.require.tlns);
-    if (path.slice(-3) != ".js") path += ".js";
-    
-    window.require.id = id;
-    window.require.modules[id] = {}; // prevent infinite loop on broken modules
-    importScripts(path);
-    return window.require(parentId, id);
-};
-function resolveModuleId(id, paths) {
-    var testPath = id, tail = "";
-    while (testPath) {
-        var alias = paths[testPath];
-        if (typeof alias == "string") {
-            return alias + tail;
-        } else if (alias) {
-            return  alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name);
-        } else if (alias === false) {
-            return "";
-        }
-        var i = testPath.lastIndexOf("/");
-        if (i === -1) break;
-        tail = testPath.substr(i) + tail;
-        testPath = testPath.slice(0, i);
-    }
-    return id;
-}
-window.require.modules = {};
-window.require.tlns = {};
-
-window.define = function(id, deps, factory) {
-    if (arguments.length == 2) {
-        factory = deps;
-        if (typeof id != "string") {
-            deps = id;
-            id = window.require.id;
-        }
-    } else if (arguments.length == 1) {
-        factory = id;
-        deps = [];
-        id = window.require.id;
-    }
-    
-    if (typeof factory != "function") {
-        window.require.modules[id] = {
-            exports: factory,
-            initialized: true
-        };
-        return;
-    }
-
-    if (!deps.length)
-        // If there is no dependencies, we inject "require", "exports" and
-        // "module" as dependencies, to provide CommonJS compatibility.
-        deps = ["require", "exports", "module"];
-
-    var req = function(childId) {
-        return window.require(id, childId);
-    };
-
-    window.require.modules[id] = {
-        exports: {},
-        factory: function() {
-            var module = this;
-            var returnExports = factory.apply(this, deps.map(function(dep) {
-                switch (dep) {
-                    // Because "require", "exports" and "module" aren't actual
-                    // dependencies, we must handle them seperately.
-                    case "require": return req;
-                    case "exports": return module.exports;
-                    case "module":  return module;
-                    // But for all other dependencies, we can just go ahead and
-                    // require them.
-                    default:        return req(dep);
-                }
-            }));
-            if (returnExports)
-                module.exports = returnExports;
-            return module;
-        }
-    };
-};
-window.define.amd = {};
-require.tlns = {};
-window.initBaseUrls  = function initBaseUrls(topLevelNamespaces) {
-    for (var i in topLevelNamespaces)
-        require.tlns[i] = topLevelNamespaces[i];
-};
-
-window.initSender = function initSender() {
-
-    var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter;
-    var oop = window.require("ace/lib/oop");
-    
-    var Sender = function() {};
-    
-    (function() {
-        
-        oop.implement(this, EventEmitter);
-                
-        this.callback = function(data, callbackId) {
-            postMessage({
-                type: "call",
-                id: callbackId,
-                data: data
-            });
-        };
-    
-        this.emit = function(name, data) {
-            postMessage({
-                type: "event",
-                name: name,
-                data: data
-            });
-        };
-        
-    }).call(Sender.prototype);
-    
-    return new Sender();
-};
-
-var main = window.main = null;
-var sender = window.sender = null;
-
-window.onmessage = function(e) {
-    var msg = e.data;
-    if (msg.event && sender) {
-        sender._signal(msg.event, msg.data);
-    }
-    else if (msg.command) {
-        if (main[msg.command])
-            main[msg.command].apply(main, msg.args);
-        else if (window[msg.command])
-            window[msg.command].apply(window, msg.args);
-        else
-            throw new Error("Unknown command:" + msg.command);
-    }
-    else if (msg.init) {
-        window.initBaseUrls(msg.tlns);
-        require("ace/lib/es5-shim");
-        sender = window.sender = window.initSender();
-        var clazz = require(msg.module)[msg.classname];
-        main = window.main = new clazz(sender);
-    }
-};
-})(this);
-
-ace.define("ace/lib/oop",["require","exports","module"], function(require, exports, module) {
-"use strict";
-
-exports.inherits = function(ctor, superCtor) {
-    ctor.super_ = superCtor;
-    ctor.prototype = Object.create(superCtor.prototype, {
-        constructor: {
-            value: ctor,
-            enumerable: false,
-            writable: true,
-            configurable: true
-        }
-    });
-};
-
-exports.mixin = function(obj, mixin) {
-    for (var key in mixin) {
-        obj[key] = mixin[key];
-    }
-    return obj;
-};
-
-exports.implement = function(proto, mixin) {
-    exports.mixin(proto, mixin);
-};
-
-});
-
-ace.define("ace/range",["require","exports","module"], function(require, exports, module) {
-"use strict";
-var comparePoints = function(p1, p2) {
-    return p1.row - p2.row || p1.column - p2.column;
-};
-var Range = function(startRow, startColumn, endRow, endColumn) {
-    this.start = {
-        row: startRow,
-        column: startColumn
-    };
-
-    this.end = {
-        row: endRow,
-        column: endColumn
-    };
-};
-
-(function() {
-    this.isEqual = function(range) {
-        return this.start.row === range.start.row &&
-            this.end.row === range.end.row &&
-            this.start.column === range.start.column &&
-            this.end.column === range.end.column;
-    };
-    this.toString = function() {
-        return ("Range: [" + this.start.row + "/" + this.start.column +
-            "] -> [" + this.end.row + "/" + this.end.column + "]");
-    };
-
-    this.contains = function(row, column) {
-        return this.compare(row, column) == 0;
-    };
-    this.compareRange = function(range) {
-        var cmp,
-            end = range.end,
-            start = range.start;
-
-        cmp = this.compare(end.row, end.column);
-        if (cmp == 1) {
-            cmp = this.compare(start.row, start.column);
-            if (cmp == 1) {
-                return 2;
-            } else if (cmp == 0) {
-                return 1;
-            } else {
-                return 0;
-            }
-        } else if (cmp == -1) {
-            return -2;
-        } else {
-            cmp = this.compare(start.row, start.column);
-            if (cmp == -1) {
-                return -1;
-            } else if (cmp == 1) {
-                return 42;
-            } else {
-                return 0;
-            }
-        }
-    };
-    this.comparePoint = function(p) {
-        return this.compare(p.row, p.column);
-    };
-    this.containsRange = function(range) {
-        return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0;
-    };
-    this.intersects = function(range) {
-        var cmp = this.compareRange(range);
-        return (cmp == -1 || cmp == 0 || cmp == 1);
-    };
-    this.isEnd = function(row, column) {
-        return this.end.row == row && this.end.column == column;
-    };
-    this.isStart = function(row, column) {
-        return this.start.row == row && this.start.column == column;
-    };
-    this.setStart = function(row, column) {
-        if (typeof row == "object") {
-            this.start.column = row.column;
-            this.start.row = row.row;
-        } else {
-            this.start.row = row;
-            this.start.column = column;
-        }
-    };
-    this.setEnd = function(row, column) {
-        if (typeof row == "object") {
-            this.end.column = row.column;
-            this.end.row = row.row;
-        } else {
-            this.end.row = row;
-            this.end.column = column;
-        }
-    };
-    this.inside = function(row, column) {
-        if (this.compare(row, column) == 0) {
-            if (this.isEnd(row, column) || this.isStart(row, column)) {
-                return false;
-            } else {
-                return true;
-            }
-        }
-        return false;
-    };
-    this.insideStart = function(row, column) {
-        if (this.compare(row, column) == 0) {
-            if (this.isEnd(row, column)) {
-                return false;
-            } else {
-                return true;
-            }
-        }
-        return false;
-    };
-    this.insideEnd = function(row, column) {
-        if (this.compare(row, column) == 0) {
-            if (this.isStart(row, column)) {
-                return false;
-            } else {
-                return true;
-            }
-        }
-        return false;
-    };
-    this.compare = function(row, column) {
-        if (!this.isMultiLine()) {
-            if (row === this.start.row) {
-                return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0);
-            }
-        }
-
-        if (row < this.start.row)
-            return -1;
-
-        if (row > this.end.row)
-            return 1;
-
-        if (this.start.row === row)
-            return column >= this.start.column ? 0 : -1;
-
-        if (this.end.row === row)
-            return column <= this.end.column ? 0 : 1;
-
-        return 0;
-    };
-    this.compareStart = function(row, column) {
-        if (this.start.row == row && this.start.column == column) {
-            return -1;
-        } else {
-            return this.compare(row, column);
-        }
-    };
-    this.compareEnd = function(row, column) {
-        if (this.end.row == row && this.end.column == column) {
-            return 1;
-        } else {
-            return this.compare(row, column);
-        }
-    };
-    this.compareInside = function(row, column) {
-        if (this.end.row == row && this.end.column == column) {
-            return 1;
-        } else if (this.start.row == row && this.start.column == column) {
-            return -1;
-        } else {
-            return this.compare(row, column);
-        }
-    };
-    this.clipRows = function(firstRow, lastRow) {
-        if (this.end.row > lastRow)
-            var end = {row: lastRow + 1, column: 0};
-        else if (this.end.row < firstRow)
-            var end = {row: firstRow, column: 0};
-
-        if (this.start.row > lastRow)
-            var start = {row: lastRow + 1, column: 0};
-        else if (this.start.row < firstRow)
-            var start = {row: firstRow, column: 0};
-
-        return Range.fromPoints(start || this.start, end || this.end);
-    };
-    this.extend = function(row, column) {
-        var cmp = this.compare(row, column);
-
-        if (cmp == 0)
-            return this;
-        else if (cmp == -1)
-            var start = {row: row, column: column};
-        else
-            var end = {row: row, column: column};
-
-        return Range.fromPoints(start || this.start, end || this.end);
-    };
-
-    this.isEmpty = function() {
-        return (this.start.row === this.end.row && this.start.column === this.end.column);
-    };
-    this.isMultiLine = function() {
-        return (this.start.row !== this.end.row);
-    };
-    this.clone = function() {
-        return Range.fromPoints(this.start, this.end);
-    };
-    this.collapseRows = function() {
-        if (this.end.column == 0)
-            return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0)
-        else
-            return new Range(this.start.row, 0, this.end.row, 0)
-    };
-    this.toScreenRange = function(session) {
-        var screenPosStart = session.documentToScreenPosition(this.start);
-        var screenPosEnd = session.documentToScreenPosition(this.end);
-
-        return new Range(
-            screenPosStart.row, screenPosStart.column,
-            screenPosEnd.row, screenPosEnd.column
-        );
-    };
-    this.moveBy = function(row, column) {
-        this.start.row += row;
-        this.start.column += column;
-        this.end.row += row;
-        this.end.column += column;
-    };
-
-}).call(Range.prototype);
-Range.fromPoints = function(start, end) {
-    return new Range(start.row, start.column, end.row, end.column);
-};
-Range.comparePoints = comparePoints;
-
-Range.comparePoints = function(p1, p2) {
-    return p1.row - p2.row || p1.column - p2.column;
-};
-
-
-exports.Range = Range;
-});
-
-ace.define("ace/apply_delta",["require","exports","module"], function(require, exports, module) {
-"use strict";
-
-function throwDeltaError(delta, errorText){
-    console.log("Invalid Delta:", delta);
-    throw "Invalid Delta: " + errorText;
-}
-
-function positionInDocument(docLines, position) {
-    return position.row    >= 0 && position.row    <  docLines.length &&
-           position.column >= 0 && position.column <= docLines[position.row].length;
-}
-
-function validateDelta(docLines, delta) {
-    if (delta.action != "insert" && delta.action != "remove")
-        throwDeltaError(delta, "delta.action must be 'insert' or 'remove'");
-    if (!(delta.lines instanceof Array))
-        throwDeltaError(delta, "delta.lines must be an Array");
-    if (!delta.start || !delta.end)
-       throwDeltaError(delta, "delta.start/end must be an present");
-    var start = delta.start;
-    if (!positionInDocument(docLines, delta.start))
-        throwDeltaError(delta, "delta.start must be contained in document");
-    var end = delta.end;
-    if (delta.action == "remove" && !positionInDocument(docLines, end))
-        throwDeltaError(delta, "delta.end must contained in document for 'remove' actions");
-    var numRangeRows = end.row - start.row;
-    var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0));
-    if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars)
-        throwDeltaError(delta, "delta.range must match delta lines");
-}
-
-exports.applyDelta = function(docLines, delta, doNotValidate) {
-    
-    var row = delta.start.row;
-    var startColumn = delta.start.column;
-    var line = docLines[row] || "";
-    switch (delta.action) {
-        case "insert":
-            var lines = delta.lines;
-            if (lines.length === 1) {
-                docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
-            } else {
-                var args = [row, 1].concat(delta.lines);
-                docLines.splice.apply(docLines, args);
-                docLines[row] = line.substring(0, startColumn) + docLines[row];
-                docLines[row + delta.lines.length - 1] += line.substring(startColumn);
-            }
-            break;
-        case "remove":
-            var endColumn = delta.end.column;
-            var endRow = delta.end.row;
-            if (row === endRow) {
-                docLines[row] = line.substring(0, startColumn) + line.substring(endColumn);
-            } else {
-                docLines.splice(
-                    row, endRow - row + 1,
-                    line.substring(0, startColumn) + docLines[endRow].substring(endColumn)
-                );
-            }
-            break;
-    }
-}
-});
-
-ace.define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) {
-"use strict";
-
-var EventEmitter = {};
-var stopPropagation = function() { this.propagationStopped = true; };
-var preventDefault = function() { this.defaultPrevented = true; };
-
-EventEmitter._emit =
-EventEmitter._dispatchEvent = function(eventName, e) {
-    this._eventRegistry || (this._eventRegistry = {});
-    this._defaultHandlers || (this._defaultHandlers = {});
-
-    var listeners = this._eventRegistry[eventName] || [];
-    var defaultHandler = this._defaultHandlers[eventName];
-    if (!listeners.length && !defaultHandler)
-        return;
-
-    if (typeof e != "object" || !e)
-        e = {};
-
-    if (!e.type)
-        e.type = eventName;
-    if (!e.stopPropagation)
-        e.stopPropagation = stopPropagation;
-    if (!e.preventDefault)
-        e.preventDefault = preventDefault;
-
-    listeners = listeners.slice();
-    for (var i=0; i<listeners.length; i++) {
-        listeners[i](e, this);
-        if (e.propagationStopped)
-            break;
-    }
-    
-    if (defaultHandler && !e.defaultPrevented)
-        return defaultHandler(e, this);
-};
-
-
-EventEmitter._signal = function(eventName, e) {
-    var listeners = (this._eventRegistry || {})[eventName];
-    if (!listeners)
-        return;
-    listeners = listeners.slice();
-    for (var i=0; i<listeners.length; i++)
-        listeners[i](e, this);
-};
-
-EventEmitter.once = function(eventName, callback) {
-    var _self = this;
-    callback && this.addEventListener(eventName, function newCallback() {
-        _self.removeEventListener(eventName, newCallback);
-        callback.apply(null, arguments);
-    });
-};
-
-
-EventEmitter.setDefaultHandler = function(eventName, callback) {
-    var handlers = this._defaultHandlers
-    if (!handlers)
-        handlers = this._defaultHandlers = {_disabled_: {}};
-    
-    if (handlers[eventName]) {
-        var old = handlers[eventName];
-        var disabled = handlers._disabled_[eventName];
-        if (!disabled)
-            handlers._disabled_[eventName] = disabled = [];
-        disabled.push(old);
-        var i = disabled.indexOf(callback);
-        if (i != -1) 
-            disabled.splice(i, 1);
-    }
-    handlers[eventName] = callback;
-};
-EventEmitter.removeDefaultHandler = function(eventName, callback) {
-    var handlers = this._defaultHandlers
-    if (!handlers)
-        return;
-    var disabled = handlers._disabled_[eventName];
-    
-    if (handlers[eventName] == callback) {
-        var old = handlers[eventName];
-        if (disabled)
-            this.setDefaultHandler(eventName, disabled.pop());
-    } else if (disabled) {
-        var i = disabled.indexOf(callback);
-        if (i != -1)
-            disabled.splice(i, 1);
-    }
-};
-
-EventEmitter.on =
-EventEmitter.addEventListener = function(eventName, callback, capturing) {
-    this._eventRegistry = this._eventRegistry || {};
-
-    var listeners = this._eventRegistry[eventName];
-    if (!listeners)
-        listeners = this._eventRegistry[eventName] = [];
-
-    if (listeners.indexOf(callback) == -1)
-        listeners[capturing ? "unshift" : "push"](callback);
-    return callback;
-};
-
-EventEmitter.off =
-EventEmitter.removeListener =
-EventEmitter.removeEventListener = function(eventName, callback) {
-    this._eventRegistry = this._eventRegistry || {};
-
-    var listeners = this._eventRegistry[eventName];
-    if (!listeners)
-        return;
-
-    var index = listeners.indexOf(callback);
-    if (index !== -1)
-        listeners.splice(index, 1);
-};
-
-EventEmitter.removeAllListeners = function(eventName) {
-    if (this._eventRegistry) this._eventRegistry[eventName] = [];
-};
-
-exports.EventEmitter = EventEmitter;
-
-});
-
-ace.define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"], function(require, exports, module) {
-"use strict";
-
-var oop = require("./lib/oop");
-var EventEmitter = require("./lib/event_emitter").EventEmitter;
-
-var Anchor = exports.Anchor = function(doc, row, column) {
-    this.$onChange = this.onChange.bind(this);
-    this.attach(doc);
-    
-    if (typeof column == "undefined")
-        this.setPosition(row.row, row.column);
-    else
-        this.setPosition(row, column);
-};
-
-(function() {
-
-    oop.implement(this, EventEmitter);
-    this.getPosition = function() {
-        return this.$clipPositionToDocument(this.row, this.column);
-    };
-    this.getDocument = function() {
-        return this.document;
-    };
-    this.$insertRight = false;
-    this.onChange = function(delta) {
-        if (delta.start.row == delta.end.row && delta.start.row != this.row)
-            return;
-
-        if (delta.start.row > this.row)
-            return;
-            
-        var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight);
-        this.setPosition(point.row, point.column, true);
-    };
-    
-    function $pointsInOrder(point1, point2, equalPointsInOrder) {
-        var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column;
-        return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter);
-    }
-            
-    function $getTransformedPoint(delta, point, moveIfEqual) {
-        var deltaIsInsert = delta.action == "insert";
-        var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row    - delta.start.row);
-        var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column);
-        var deltaStart = delta.start;
-        var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range.
-        if ($pointsInOrder(point, deltaStart, moveIfEqual)) {
-            return {
-                row: point.row,
-                column: point.column
-            };
-        }
-        if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) {
-            return {
-                row: point.row + deltaRowShift,
-                column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0)
-            };
-        }
-        
-        return {
-            row: deltaStart.row,
-            column: deltaStart.column
-        };
-    }
-    this.setPosition = function(row, column, noClip) {
-        var pos;
-        if (noClip) {
-            pos = {
-                row: row,
-                column: column
-            };
-        } else {
-            pos = this.$clipPositionToDocument(row, column);
-        }
-
-        if (this.row == pos.row && this.column == pos.column)
-            return;
-
-        var old = {
-            row: this.row,
-            column: this.column
-        };
-
-        this.row = pos.row;
-        this.column = pos.column;
-        this._signal("change", {
-            old: old,
-            value: pos
-        });
-    };
-    this.detach = function() {
-        this.document.removeEventListener("change", this.$onChange);
-    };
-    this.attach = function(doc) {
-        this.document = doc || this.document;
-        this.document.on("change", this.$onChange);
-    };
-    this.$clipPositionToDocument = function(row, column) {
-        var pos = {};
-
-        if (row >= this.document.getLength()) {
-            pos.row = Math.max(0, this.document.getLength() - 1);
-            pos.column = this.document.getLine(pos.row).length;
-        }
-        else if (row < 0) {
-            pos.row = 0;
-            pos.column = 0;
-        }
-        else {
-            pos.row = row;
-            pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column));
-        }
-
-        if (column < 0)
-            pos.column = 0;
-
-        return pos;
-    };
-
-}).call(Anchor.prototype);
-
-});
-
-ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) {
-"use strict";
-
-var oop = require("./lib/oop");
-var applyDelta = require("./apply_delta").applyDelta;
-var EventEmitter = require("./lib/event_emitter").EventEmitter;
-var Range = require("./range").Range;
-var Anchor = require("./anchor").Anchor;
-
-var Document = function(textOrLines) {
-    this.$lines = [""];
-    if (textOrLines.length === 0) {
-        this.$lines = [""];
-    } else if (Array.isArray(textOrLines)) {
-        this.insertMergedLines({row: 0, column: 0}, textOrLines);
-    } else {
-        this.insert({row: 0, column:0}, textOrLines);
-    }
-};
-
-(function() {
-
-    oop.implement(this, EventEmitter);
-    this.setValue = function(text) {
-        var len = this.getLength() - 1;
-        this.remove(new Range(0, 0, len, this.getLine(len).length));
-        this.insert({row: 0, column: 0}, text);
-    };
-    this.getValue = function() {
-        return this.getAllLines().join(this.getNewLineCharacter());
-    };
-    this.createAnchor = function(row, column) {
-        return new Anchor(this, row, column);
-    };
-    if ("aaa".split(/a/).length === 0) {
-        this.$split = function(text) {
-            return text.replace(/\r\n|\r/g, "\n").split("\n");
-        };
-    } else {
-        this.$split = function(text) {
-            return text.split(/\r\n|\r|\n/);
-        };
-    }
-
-
-    this.$detectNewLine = function(text) {
-        var match = text.match(/^.*?(\r\n|\r|\n)/m);
-        this.$autoNewLine = match ? match[1] : "\n";
-        this._signal("changeNewLineMode");
-    };
-    this.getNewLineCharacter = function() {
-        switch (this.$newLineMode) {
-          case "windows":
-            return "\r\n";
-          case "unix":
-            return "\n";
-          default:
-            return this.$autoNewLine || "\n";
-        }
-    };
-
-    this.$autoNewLine = "";
-    this.$newLineMode = "auto";
-    this.setNewLineMode = function(newLineMode) {
-        if (this.$newLineMode === newLineMode)
-            return;
-
-        this.$newLineMode = newLineMode;
-        this._signal("changeNewLineMode");
-    };
-    this.getNewLineMode = function() {
-        return this.$newLineMode;
-    };
-    this.isNewLine = function(text) {
-        return (text == "\r\n" || text == "\r" || text == "\n");
-    };
-    this.getLine = function(row) {
-        return this.$lines[row] || "";
-    };
-    this.getLines = function(firstRow, lastRow) {
-        return this.$lines.slice(firstRow, lastRow + 1);
-    };
-    this.getAllLines = function() {
-        return this.getLines(0, this.getLength());
-    };
-    this.getLength = function() {
-        return this.$lines.length;
-    };
-    this.getTextRange = function(range) {
-        return this.getLinesForRange(range).join(this.getNewLineCharacter());
-    };
-    this.getLinesForRange = function(range) {
-        var lines;
-        if (range.start.row === range.end.row) {
-            lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)];
-        } else {
-            lines = this.getLines(range.start.row, range.end.row);
-            lines[0] = (lines[0] || "").substring(range.start.column);
-            var l = lines.length - 1;
-            if (range.end.row - range.start.row == l)
-                lines[l] = lines[l].substring(0, range.end.column);
-        }
-        return lines;
-    };
-    this.insertLines = function(row, lines) {
-        console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead.");
-        return this.insertFullLines(row, lines);
-    };
-    this.removeLines = function(firstRow, lastRow) {
-        console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead.");
-        return this.removeFullLines(firstRow, lastRow);
-    };
-    this.insertNewLine = function(position) {
-        console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead.");
-        return this.insertMergedLines(position, ["", ""]);
-    };
-    this.insert = function(position, text) {
-        if (this.getLength() <= 1)
-            this.$detectNewLine(text);
-        
-        return this.insertMergedLines(position, this.$split(text));
-    };
-    this.insertInLine = function(position, text) {
-        var start = this.clippedPos(position.row, position.column);
-        var end = this.pos(position.row, position.column + text.length);
-        
-        this.applyDelta({
-            start: start,
-            end: end,
-            action: "insert",
-            lines: [text]
-        }, true);
-        
-        return this.clonePos(end);
-    };
-    
-    this.clippedPos = function(row, column) {
-        var length = this.getLength();
-        if (row === undefined) {
-            row = length;
-        } else if (row < 0) {
-            row = 0;
-        } else if (row >= length) {
-            row = length - 1;
-            column = undefined;
-        }
-        var line = this.getLine(row);
-        if (column == undefined)
-            column = line.length;
-        column = Math.min(Math.max(column, 0), line.length);
-        return {row: row, column: column};
-    };
-    
-    this.clonePos = function(pos) {
-        return {row: pos.row, column: pos.column};
-    };
-    
-    this.pos = function(row, column) {
-        return {row: row, column: column};
-    };
-    
-    this.$clipPosition = function(position) {
-        var length = this.getLength();
-        if (position.row >= length) {
-            position.row = Math.max(0, length - 1);
-            position.column = this.getLine(length - 1).length;
-        } else {
-            position.row = Math.max(0, position.row);
-            position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length);
-        }
-        return position;
-    };
-    this.insertFullLines = function(row, lines) {
-        row = Math.min(Math.max(row, 0), this.getLength());
-        var column = 0;
-        if (row < this.getLength()) {
-            lines = lines.concat([""]);
-            column = 0;
-        } else {
-            lines = [""].concat(lines);
-            row--;
-            column = this.$lines[row].length;
-        }
-        this.insertMergedLines({row: row, column: column}, lines);
-    };    
-    this.insertMergedLines = function(position, lines) {
-        var start = this.clippedPos(position.row, position.column);
-        var end = {
-            row: start.row + lines.length - 1,
-            column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length
-        };
-        
-        this.applyDelta({
-            start: start,
-            end: end,
-            action: "insert",
-            lines: lines
-        });
-        
-        return this.clonePos(end);
-    };
-    this.remove = function(range) {
-        var start = this.clippedPos(range.start.row, range.start.column);
-        var end = this.clippedPos(range.end.row, range.end.column);
-        this.applyDelta({
-            start: start,
-            end: end,
-            action: "remove",
-            lines: this.getLinesForRange({start: start, end: end})
-        });
-        return this.clonePos(start);
-    };
-    this.removeInLine = function(row, startColumn, endColumn) {
-        var start = this.clippedPos(row, startColumn);
-        var end = this.clippedPos(row, endColumn);
-        
-        this.applyDelta({
-            start: start,
-            end: end,
-            action: "remove",
-            lines: this.getLinesForRange({start: start, end: end})
-        }, true);
-        
-        return this.clonePos(start);
-    };
-    this.removeFullLines = function(firstRow, lastRow) {
-        firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1);
-        lastRow  = Math.min(Math.max(0, lastRow ), this.getLength() - 1);
-        var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0;
-        var deleteLastNewLine  = lastRow  < this.getLength() - 1;
-        var startRow = ( deleteFirstNewLine ? firstRow - 1                  : firstRow                    );
-        var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0                           );
-        var endRow   = ( deleteLastNewLine  ? lastRow + 1                   : lastRow                     );
-        var endCol   = ( deleteLastNewLine  ? 0                             : this.getLine(endRow).length ); 
-        var range = new Range(startRow, startCol, endRow, endCol);
-        var deletedLines = this.$lines.slice(firstRow, lastRow + 1);
-        
-        this.applyDelta({
-            start: range.start,
-            end: range.end,
-            action: "remove",
-            lines: this.getLinesForRange(range)
-        });
-        return deletedLines;
-    };
-    this.removeNewLine = function(row) {
-        if (row < this.getLength() - 1 && row >= 0) {
-            this.applyDelta({
-                start: this.pos(row, this.getLine(row).length),
-                end: this.pos(row + 1, 0),
-                action: "remove",
-                lines: ["", ""]
-            });
-        }
-    };
-    this.replace = function(range, text) {
-        if (!(range instanceof Range))
-            range = Range.fromPoints(range.start, range.end);
-        if (text.length === 0 && range.isEmpty())
-            return range.start;
-        if (text == this.getTextRange(range))
-            return range.end;
-
-        this.remove(range);
-        var end;
-        if (text) {
-            end = this.insert(range.start, text);
-        }
-        else {
-            end = range.start;
-        }
-        
-        return end;
-    };
-    this.applyDeltas = function(deltas) {
-        for (var i=0; i<deltas.length; i++) {
-            this.applyDelta(deltas[i]);
-        }
-    };
-    this.revertDeltas = function(deltas) {
-        for (var i=deltas.length-1; i>=0; i--) {
-            this.revertDelta(deltas[i]);
-        }
-    };
-    this.applyDelta = function(delta, doNotValidate) {
-        var isInsert = delta.action == "insert";
-        if (isInsert ? delta.lines.length <= 1 && !delta.lines[0]
-            : !Range.comparePoints(delta.start, delta.end)) {
-            return;
-        }
-        
-        if (isInsert && delta.lines.length > 20000)
-            this.$splitAndapplyLargeDelta(delta, 20000);
-        applyDelta(this.$lines, delta, doNotValidate);
-        this._signal("change", delta);
-    };
-    
-    this.$splitAndapplyLargeDelta = function(delta, MAX) {
-        var lines = delta.lines;
-        var l = lines.length;
-        var row = delta.start.row; 
-        var column = delta.start.column;
-        var from = 0, to = 0;
-        do {
-            from = to;
-            to += MAX - 1;
-            var chunk = lines.slice(from, to);
-            if (to > l) {
-                delta.lines = chunk;
-                delta.start.row = row + from;
-                delta.start.column = column;
-                break;
-            }
-            chunk.push("");
-            this.applyDelta({
-                start: this.pos(row + from, column),
-                end: this.pos(row + to, column = 0),
-                action: delta.action,
-                lines: chunk
-            }, true);
-        } while(true);
-    };
-    this.revertDelta = function(delta) {
-        this.applyDelta({
-            start: this.clonePos(delta.start),
-            end: this.clonePos(delta.end),
-            action: (delta.action == "insert" ? "remove" : "insert"),
-            lines: delta.lines.slice()
-        });
-    };
-    this.indexToPosition = function(index, startRow) {
-        var lines = this.$lines || this.getAllLines();
-        var newlineLength = this.getNewLineCharacter().length;
-        for (var i = startRow || 0, l = lines.length; i < l; i++) {
-            index -= lines[i].length + newlineLength;
-            if (index < 0)
-                return {row: i, column: index + lines[i].length + newlineLength};
-        }
-        return {row: l-1, column: lines[l-1].length};
-    };
-    this.positionToIndex = function(pos, startRow) {
-        var lines = this.$lines || this.getAllLines();
-        var newlineLength = this.getNewLineCharacter().length;
-        var index = 0;
-        var row = Math.min(pos.row, lines.length);
-        for (var i = startRow || 0; i < row; ++i)
-            index += lines[i].length + newlineLength;
-
-        return index + pos.column;
-    };
-
-}).call(Document.prototype);
-
-exports.Document = Document;
-});
-
-ace.define("ace/lib/lang",["require","exports","module"], function(require, exports, module) {
-"use strict";
-
-exports.last = function(a) {
-    return a[a.length - 1];
-};
-
-exports.stringReverse = function(string) {
-    return string.split("").reverse().join("");
-};
-
-exports.stringRepeat = function (string, count) {
-    var result = '';
-    while (count > 0) {
-        if (count & 1)
-            result += string;
-
-        if (count >>= 1)
-            string += string;
-    }
-    return result;
-};
-
-var trimBeginRegexp = /^\s\s*/;
-var trimEndRegexp = /\s\s*$/;
-
-exports.stringTrimLeft = function (string) {
-    return string.replace(trimBeginRegexp, '');
-};
-
-exports.stringTrimRight = function (string) {
-    return string.replace(trimEndRegexp, '');
-};
-
-exports.copyObject = function(obj) {
-    var copy = {};
-    for (var key in obj) {
-        copy[key] = obj[key];
-    }
-    return copy;
-};
-
-exports.copyArray = function(array){
-    var copy = [];
-    for (var i=0, l=array.length; i<l; i++) {
-        if (array[i] && typeof array[i] == "object")
-            copy[i] = this.copyObject(array[i]);
-        else 
-            copy[i] = array[i];
-    }
-    return copy;
-};
-
-exports.deepCopy = function deepCopy(obj) {
-    if (typeof obj !== "object" || !obj)
-        return obj;
-    var copy;
-    if (Array.isArray(obj)) {
-        copy = [];
-        for (var key = 0; key < obj.length; key++) {
-            copy[key] = deepCopy(obj[key]);
-        }
-        return copy;
-    }
-    if (Object.prototype.toString.call(obj) !== "[object Object]")
-        return obj;
-    
-    copy = {};
-    for (var key in obj)
-        copy[key] = deepCopy(obj[key]);
-    return copy;
-};
-
-exports.arrayToMap = function(arr) {
-    var map = {};
-    for (var i=0; i<arr.length; i++) {
-        map[arr[i]] = 1;
-    }
-    return map;
-
-};
-
-exports.createMap = function(props) {
-    var map = Object.create(null);
-    for (var i in props) {
-        map[i] = props[i];
-    }
-    return map;
-};
-exports.arrayRemove = function(array, value) {
-  for (var i = 0; i <= array.length; i++) {
-    if (value === array[i]) {
-      array.splice(i, 1);
-    }
-  }
-};
-
-exports.escapeRegExp = function(str) {
-    return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
-};
-
-exports.escapeHTML = function(str) {
-    return str.replace(/&/g, "&#38;").replace(/"/g, "&#34;").replace(/'/g, "&#39;").replace(/</g, "&#60;");
-};
-
-exports.getMatchOffsets = function(string, regExp) {
-    var matches = [];
-
-    string.replace(regExp, function(str) {
-        matches.push({
-            offset: arguments[arguments.length-2],
-            length: str.length
-        });
-    });
-
-    return matches;
-};
-exports.deferredCall = function(fcn) {
-    var timer = null;
-    var callback = function() {
-        timer = null;
-        fcn();
-    };
-
-    var deferred = function(timeout) {
-        deferred.cancel();
-        timer = setTimeout(callback, timeout || 0);
-        return deferred;
-    };
-
-    deferred.schedule = deferred;
-
-    deferred.call = function() {
-        this.cancel();
-        fcn();
-        return deferred;
-    };
-
-    deferred.cancel = function() {
-        clearTimeout(timer);
-        timer = null;
-        return deferred;
-    };
-    
-    deferred.isPending = function() {
-        return timer;
-    };
-
-    return deferred;
-};
-
-
-exports.delayedCall = function(fcn, defaultTimeout) {
-    var timer = null;
-    var callback = function() {
-        timer = null;
-        fcn();
-    };
-
-    var _self = function(timeout) {
-        if (timer == null)
-            timer = setTimeout(callback, timeout || defaultTimeout);
-    };
-
-    _self.delay = function(timeout) {
-        timer && clearTimeout(timer);
-        timer = setTimeout(callback, timeout || defaultTimeout);
-    };
-    _self.schedule = _self;
-
-    _self.call = function() {
-        this.cancel();
-        fcn();
-    };
-
-    _self.cancel = function() {
-        timer && clearTimeout(timer);
-        timer = null;
-    };
-
-    _self.isPending = function() {
-        return timer;
-    };
-
-    return _self;
-};
-});
-
-ace.define("ace/worker/mirror",["require","exports","module","ace/range","ace/document","ace/lib/lang"], function(require, exports, module) {
-"use strict";
-
-var Range = require("../range").Range;
-var Document = require("../document").Document;
-var lang = require("../lib/lang");
-    
-var Mirror = exports.Mirror = function(sender) {
-    this.sender = sender;
-    var doc = this.doc = new Document("");
-    
-    var deferredUpdate = this.deferredUpdate = lang.delayedCall(this.onUpdate.bind(this));
-    
-    var _self = this;
-    sender.on("change", function(e) {
-        var data = e.data;
-        if (data[0].start) {
-            doc.applyDeltas(data);
-        } else {
-            for (var i = 0; i < data.length; i += 2) {
-                if (Array.isArray(data[i+1])) {
-                    var d = {action: "insert", start: data[i], lines: data[i+1]};
-                } else {
-                    var d = {action: "remove", start: data[i], end: data[i+1]};
-                }
-                doc.applyDelta(d, true);
-            }
-        }
-        if (_self.$timeout)
-            return deferredUpdate.schedule(_self.$timeout);
-        _self.onUpdate();
-    });
-};
-
-(function() {
-    
-    this.$timeout = 500;
-    
-    this.setTimeout = function(timeout) {
-        this.$timeout = timeout;
-    };
-    
-    this.setValue = function(value) {
-        this.doc.setValue(value);
-        this.deferredUpdate.schedule(this.$timeout);
-    };
-    
-    this.getValue = function(callbackId) {
-        this.sender.callback(this.doc.getValue(), callbackId);
-    };
-    
-    this.onUpdate = function() {
-    };
-    
-    this.isPending = function() {
-        return this.deferredUpdate.isPending();
-    };
-    
-}).call(Mirror.prototype);
-
-});
-
-ace.define("ace/mode/latex_beta_worker",["require","exports","module","ace/lib/oop","ace/worker/mirror"], function(require, exports, module) {
-"use strict";
-
-var oop = require("../lib/oop");
-var Mirror = require("../worker/mirror").Mirror;
-
-var LatexWorker = exports.LatexWorker = function(sender) {
-    Mirror.call(this, sender);
-    this.setTimeout(250);
-};
-
-oop.inherits(LatexWorker, Mirror);
-
-var Tokenise = function (text) {
-    var Tokens = [];
-    var Comments = [];
-    var pos = -1;
-    var SPECIAL = /[\\\{\}\$\&\#\^\_\~\%]/g;  // match TeX special characters
-    var NEXTCS = /[^a-zA-Z]/g;  // match characters which aren't part of a TeX control sequence
-    var idx = 0;
-
-    var lineNumber = 0;   // current line number when parsing tokens (zero-based)
-    var linePosition = [];  // mapping from line number to absolute offset of line in text[]
-    linePosition[0] = 0;
-
-    var checkingDisabled = false;
-    var count = 0;  // number of tokens parses
-    var MAX_TOKENS = 100000;
-
-    while (true) {
-        count++;
-        if (count > MAX_TOKENS) {
-            throw new Error("exceed max token count of " + MAX_TOKENS);
-            break;
-        };
-        var result = SPECIAL.exec(text);
-        if (result == null) {
-            if (idx < text.length) {
-                Tokens.push([lineNumber, "Text", idx, text.length]);
-            }
-            break;
-        }
-        if (result && result.index <= pos) {
-            throw new Error("infinite loop in parsing");
-            break;
-        };
-        pos = result.index;
-        if (pos > idx) {
-            Tokens.push([lineNumber, "Text", idx, pos]);
-        }
-        for (var i = idx; i < pos; i++) {
-            if (text[i] === "\n") {
-                lineNumber++;
-                linePosition[lineNumber] = i+1;
-            }
-        }
-
-        var newIdx = SPECIAL.lastIndex;
-        idx = newIdx;
-        var code = result[0];
-        if (code === "%") { // comment character
-            var newLinePos = text.indexOf("\n", idx);
-            if (newLinePos === -1) {
-                newLinePos = text.length;
-            };
-            var commentString = text.substring(idx, newLinePos);
-            if (commentString.indexOf("%novalidate") === 0) {
-                return [];
-            } else if(!checkingDisabled && commentString.indexOf("%begin novalidate") === 0) {
-                checkingDisabled = true;
-            } else if (checkingDisabled && commentString.indexOf("%end novalidate") === 0) {
-                checkingDisabled = false;
-            };
-            idx = SPECIAL.lastIndex = newLinePos + 1;
-            Comments.push([lineNumber, idx, newLinePos]);
-            lineNumber++;
-            linePosition[lineNumber] = idx;
-        } else if (checkingDisabled) {
-            continue;
-        } else if (code === '\\') { // escape character
-            NEXTCS.lastIndex = idx;
-            var controlSequence = NEXTCS.exec(text);
-            var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
-            if (nextSpecialPos === idx) {
-                Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]);
-                idx = SPECIAL.lastIndex = idx + 1;
-                char = text[nextSpecialPos];
-                if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
-            } else {
-                Tokens.push([lineNumber, code, pos, nextSpecialPos, text.slice(idx, nextSpecialPos)]);
-                var char;
-                while ((char = text[nextSpecialPos]) === ' ' || char === '\t' || char  === '\r' || char === '\n') {
-                    nextSpecialPos++;
-                    if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
-                }
-                idx = SPECIAL.lastIndex = nextSpecialPos;
-            }
-        } else if (code === "{") {  // open group
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "}") {  // close group
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "$") {  // math mode
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "&") {  // tabalign
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "#") {  // macro parameter
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "^") {  // superscript
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "_") {  // subscript
-            Tokens.push([lineNumber, code, pos]);
-        } else if (code === "~") {  // active character (space)
-            Tokens.push([lineNumber, code, pos]);
-        } else {
-            throw "unrecognised character " + code;
-        }
-    }
-
-    return {tokens: Tokens, comments: Comments, linePosition: linePosition, lineNumber: lineNumber, text: text};
-};
-
-var read1arg = function (TokeniseResult, k, options) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-    if (options && options.allowStar) {
-        var optional = Tokens[k+1];
-        if (optional && optional[1] === "Text") {
-            var optionalstr = text.substring(optional[2], optional[3]);
-            if (optionalstr === "*") { k++;}
-        };
-    };
-
-    var open = Tokens[k+1];
-    var env = Tokens[k+2];
-    var close = Tokens[k+3];
-    var envName;
-
-    if(open && open[1] === "\\") {
-        envName = open[4]; // array element 4 is command sequence
-        return k + 1;
-    } else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") {
-        envName = env[4]; // NOTE: if we were actually using this, keep track of * above
-        return k + 3; // array element 4 is command sequence
-    } else {
-        return null;
-    }
-};
-
-var readLetDefinition = function (TokeniseResult, k) {
-
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var first = Tokens[k+1];
-    var second = Tokens[k+2];
-    var third = Tokens[k+3];
-
-    if(first && first[1] === "\\" && second && second[1] === "\\") {
-        return k + 2;
-    } else if(first && first[1] === "\\" &&
-              second && second[1] === "Text" && text.substring(second[2], second[3]) === "=" &&
-              third && third[1] === "\\") {
-        return k + 3;
-    } else {
-        return null;
-    }
-};
-
-var read1name = function (TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var open = Tokens[k+1];
-    var env = Tokens[k+2];
-    var close = Tokens[k+3];
-
-    if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
-        var envName = text.substring(env[2], env[3]);
-        return k + 3;
-    } else if (open && open[1] === "{" && env && env[1] === "Text") {
-        envName = "";
-        for (var j = k + 2, tok; (tok = Tokens[j]); j++) {
-            if (tok[1] === "Text") {
-                var str = text.substring(tok[2], tok[3]);
-                if (!str.match(/^\S*$/)) { break; }
-                envName = envName + str;
-            } else if (tok[1] === "_") {
-                envName = envName + "_";
-            } else {
-                break;
-            }
-        }
-        if (tok && tok[1] === "}") {
-            return  j; // advance past these tokens
-        } else {
-            return null;
-        }
-    } else {
-        return null;
-    }
-};
-
-var read1filename = function (TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var fileName = "";
-    for (var j = k + 1, tok; (tok = Tokens[j]); j++) {
-        if (tok[1] === "Text") {
-            var str = text.substring(tok[2], tok[3]);
-            if (!str.match(/^\S*$/)) { break; }
-            fileName = fileName + str;
-        } else if (tok[1] === "_") {
-            fileName = fileName + "_";
-        } else {
-            break;
-        }
-    }
-    if (fileName.length > 0) {
-        return  j; // advance past these tokens
-    } else {
-        return null;
-    }
-};
-
-var readOptionalParams = function(TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var params = Tokens[k+1];
-
-    if(params && params[1] === "Text") {
-        var paramNum = text.substring(params[2], params[3]);
-        if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
-            return k + 1; // got it
-        };
-    };
-    var count = 0;
-    var nextToken = Tokens[k+1];
-    var pos = nextToken[2];
-
-    for (var i = pos, end = text.length; i < end; i++) {
-        var char = text[i];
-        if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
-        if (char === "[") { count++; }
-        if (char === "]") { count--; }
-        if (count === 0 && char === "{") { return k - 1; }
-        if (count > 0 && (char  === '\r' || char === '\n')) { return null; }
-    };
-    return null;
-};
-
-var readOptionalGeneric = function(TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var params = Tokens[k+1];
-
-    if(params && params[1] === "Text") {
-        var paramNum = text.substring(params[2], params[3]);
-        if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) {
-            return k + 1; // got it
-        };
-    };
-    return null;
-};
-
-var readOptionalDef = function (TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var defToken = Tokens[k];
-    var pos = defToken[3];
-
-    var openBrace = "{";
-    var nextToken = Tokens[k+1];
-    for (var i = pos, end = text.length; i < end; i++) {
-        var char = text[i];
-        if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
-        if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments
-        if (char  === '\r' || char === '\n') { return null; }
-    };
-
-    return null;
-
-};
-
-var readDefinition = function(TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    k = k + 1;
-    var count = 0;
-    var nextToken = Tokens[k];
-    while (nextToken && nextToken[1] === "Text") {
-        var start = nextToken[2], end = nextToken[3];
-        for (var i = start; i < end; i++) {
-            var char = text[i];
-            if (char === ' ' || char === '\t' || char  === '\r' || char === '\n') { continue; }
-            return null; // bail out, should begin with a {
-        }
-        k++;
-        nextToken = Tokens[k];
-    }
-    if (nextToken && nextToken[1] === "{") {
-        count++;
-        while (count>0) {
-            k++;
-            nextToken = Tokens[k];
-            if(!nextToken) { break; };
-            if (nextToken[1] === "}") { count--; }
-            if (nextToken[1] === "{") { count++; }
-        }
-        return k;
-    }
-
-    return null;
-};
-
-var readVerb = function(TokeniseResult, k) {
-
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var verbToken = Tokens[k];
-    var verbStr = text.substring(verbToken[2], verbToken[3]);
-    var pos = verbToken[3];
-    if (text[pos] === "*") { pos++; } // \verb* form of command
-    var delimiter = text[pos];
-    pos++;
-
-    var nextToken = Tokens[k+1];
-    for (var i = pos, end = text.length; i < end; i++) {
-        var char = text[i];
-        if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
-        if (char === delimiter) { return k; };
-        if (char  === '\r' || char === '\n') { return null; }
-    };
-
-    return null;
-};
-
-var readUrl = function(TokeniseResult, k) {
-
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var urlToken = Tokens[k];
-    var urlStr = text.substring(urlToken[2], urlToken[3]);
-    var pos = urlToken[3];
-    var openDelimiter = text[pos];
-    var closeDelimiter =  (openDelimiter === "{") ? "}" : openDelimiter;
-    var nextToken = Tokens[k+1];
-    if (nextToken && pos === nextToken[2]) {
-        k++;
-        nextToken = Tokens[k+1];
-    };
-    pos++;
-
-    var count = 1;
-    for (var i = pos, end = text.length; count > 0 && i < end; i++) {
-        var char = text[i];
-        if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
-        if (char === closeDelimiter) {
-            count--;
-        } else if (char === openDelimiter) {
-            count++;
-        };
-        if (count === 0) { return k; };
-        if (char  === '\r' || char === '\n') { return null; }
-    };
-
-    return null;
-};
-
-var InterpretTokens = function (TokeniseResult, ErrorReporter) {
-    var Tokens = TokeniseResult.tokens;
-    var linePosition = TokeniseResult.linePosition;
-    var lineNumber = TokeniseResult.lineNumber;
-    var text = TokeniseResult.text;
-
-    var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
-    var TokenError = ErrorReporter.TokenError;
-    var Environments = new EnvHandler(ErrorReporter);
-
-    var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
-    var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes
-    var seenUserDefinedBeginEquation = false; // if we have seen macros like \beq
-    var seenUserDefinedEndEquation = false; // if we have seen macros like \eeq
-
-    for (var i = 0, len = Tokens.length; i < len; i++) {
-        var token = Tokens[i];
-        var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
-
-        if (type === "{") {
-            Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
-            nextGroupMathModeStack.push(nextGroupMathMode);
-            nextGroupMathMode = null;
-            continue;
-        } else if (type === "}") {
-            Environments.push({command:"}", token:token});
-            nextGroupMathMode = nextGroupMathModeStack.pop();
-            continue;
-        } else {
-            nextGroupMathMode = null;
-        };
-
-        if (type === "\\") {
-            if (seq === "begin" || seq === "end") {
-                var open = Tokens[i+1];
-                var env = Tokens[i+2];
-                var close = Tokens[i+3];
-                if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
-                    var envName = text.substring(env[2], env[3]);
-                    Environments.push({command: seq, name: envName, token: token, closeToken: close});
-                    i = i + 3; // advance past these tokens
-                } else {
-                    if (open && open[1] === "{" && env && env[1] === "Text") {
-                        envName = "";
-                        for (var j = i + 2, tok; (tok = Tokens[j]); j++) {
-                            if (tok[1] === "Text") {
-                                var str = text.substring(tok[2], tok[3]);
-                                if (!str.match(/^\S*$/)) { break; }
-                                envName = envName + str;
-                            } else if (tok[1] === "_") {
-                                envName = envName + "_";
-                            } else {
-                                break;
-                            }
-                        }
-                        if (tok && tok[1] === "}") {
-                            Environments.push({command: seq, name: envName, token: token, closeToken: close});
-                            i = j; // advance past these tokens
-                            continue;
-                        }
-                    }
-                    var endToken = null;
-                    if (open && open[1] === "{") {
-                        endToken = open; // we've got a {
-                        if (env && env[1] === "Text") {
-                            endToken = env.slice(); // we've got some text following the {
-                            start = endToken[2]; end = endToken[3];
-                            for (j = start; j < end; j++) {
-                                var char = text[j];
-                                if (char === ' ' || char === '\t' || char  === '\r' || char === '\n') { break; }
-                            }
-                            endToken[3] = j; // the end of partial token is as far as we got looking ahead
-                        };
-                    };
-
-                    if (endToken) {
-                        TokenErrorFromTo(token, endToken, "invalid environment command " + text.substring(token[2], endToken[3] || endToken[2]));
-                    } else {
-                        TokenError(token, "invalid environment command");
-                    };
-                }
-            } else if (typeof seq === "string" && seq.match(/^(be|beq|beqa|bea)$/i)) {
-                seenUserDefinedBeginEquation = true;
-            } else if (typeof seq === "string" && seq.match(/^(ee|eeq|eeqn|eeqa|eeqan|eea)$/i)) {
-                seenUserDefinedEndEquation = true;
-            } else if (seq === "newcommand" || seq === "renewcommand" || seq === "DeclareRobustCommand") {
-                var newPos = read1arg(TokeniseResult, i, {allowStar: true});
-                if (newPos === null) { continue; } else {i = newPos;};
-                newPos = readOptionalParams(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-
-            } else if (seq === "def") {
-                newPos = read1arg(TokeniseResult, i);
-                if (newPos === null) { continue; } else {i = newPos;};
-                newPos = readOptionalDef(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-
-            } else if (seq === "let") {
-                newPos = readLetDefinition(TokeniseResult, i);
-                if (newPos === null) { continue; } else {i = newPos;};
-
-            } else if (seq === "newcolumntype") {
-                newPos = read1name(TokeniseResult, i);
-                if (newPos === null) { continue; } else {i = newPos;};
-                newPos = readOptionalParams(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-
-            } else if (seq === "newenvironment" || seq === "renewenvironment") {
-                newPos = read1name(TokeniseResult, i);
-                if (newPos === null) { continue; } else {i = newPos;};
-                newPos = readOptionalParams(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-            } else if (seq === "verb") {
-                newPos = readVerb(TokeniseResult, i);
-                if (newPos === null) { TokenError(token, "invalid verbatim command"); } else {i = newPos;};
-            } else if (seq === "url") {
-                newPos = readUrl(TokeniseResult, i);
-                if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;};
-            } else if (seq === "left" || seq === "right") {
-                var nextToken = Tokens[i+1];
-                char = "";
-                if (nextToken && nextToken[1] === "Text") {
-                    char = text.substring(nextToken[2], nextToken[2] + 1);
-                } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") {
-                    char = nextToken[4];
-                } else if (nextToken && nextToken[1] === "\\") {
-                    char = "unknown";
-                }
-                if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) {
-                    TokenError(token, "invalid bracket command");
-                } else {
-                    i = i + 1;
-                    Environments.push({command:seq, token:token});
-                };
-            } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") {
-                Environments.push({command:seq, token:token});
-            } else if (seq === "input") {
-                newPos = read1filename(TokeniseResult, i);
-                if (newPos === null) { continue; } else {i = newPos;};
-            } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") {
-                nextGroupMathMode = false;
-            } else if (seq === "rotatebox" || seq === "scalebox") {
-                newPos = readOptionalGeneric(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                nextGroupMathMode = false;
-            } else if (seq === "resizebox") {
-                newPos = readOptionalGeneric(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-
-                nextGroupMathMode = false;
-            } else if (seq === "DeclareMathOperator") {
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-            } else if (seq === "DeclarePairedDelimiter") {
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-                newPos = readDefinition(TokeniseResult, i);
-                if (newPos === null) { /* do nothing */ } else {i = newPos;};
-            } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) {
-                var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode === null) {
-                    TokenError(token, type + seq + " must be inside math mode", {mathMode:true});
-                };
-            } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) {
-                currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode) {
-                    TokenError(token, type + seq + " used inside math mode", {mathMode:true});
-                    Environments.resetMathMode();
-                };
-            } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) {
-                nextGroupMathMode = undefined;
-            };
-
-        } else if (type === "$") {
-            var lookAhead = Tokens[i+1];
-            var nextIsDollar = lookAhead && lookAhead[1] === "$";
-            currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-            if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) {
-                Environments.push({command:"$$", token:token});
-                i = i + 1;
-            } else {
-                Environments.push({command:"$", token:token});
-            }
-        } else if (type === "^" || type === "_") {
-            currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-            var insideGroup = Environments.insideGroup();  // true if inside {....}
-            if (currentMathMode === null && !insideGroup) {
-                TokenError(token, type + " must be inside math mode", {mathMode:true});
-            };
-        }
-    };
-
-    if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) {
-        ErrorReporter.filterMath = true;
-    };
-
-    return Environments;
-};
-
-var EnvHandler = function (ErrorReporter) {
-    var ErrorTo = ErrorReporter.EnvErrorTo;
-    var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
-    var ErrorFrom = ErrorReporter.EnvErrorFrom;
-
-    var envs = [];
-
-    var state = [];
-    var documentClosed = null;
-    var inVerbatim = false;
-    var verbatimRanges = [];
-
-    this.Environments = envs;
-
-    this.push = function (newEnv) {
-        this.setEnvProps(newEnv);
-        this.checkAndUpdateState(newEnv);
-        envs.push(newEnv);
-    };
-
-    this._endVerbatim = function (thisEnv) {
-        var lastEnv = state.pop();
-        if (lastEnv && lastEnv.name === thisEnv.name) {
-            inVerbatim = false;
-            verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
-        } else {
-            if(lastEnv) { state.push(lastEnv); } ;
-        }
-    };
-
-    var invalidEnvs = [];
-
-    this._end = function (thisEnv) {
-        do {
-            var lastEnv = state.pop();
-            var retry = false;
-            var i;
-
-            if (closedBy(lastEnv, thisEnv)) {
-                if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
-                    documentClosed = thisEnv;
-                };
-                return;
-            } else if (!lastEnv) {
-                if (documentClosed) {
-                    ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
-                } else {
-                    ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
-                }
-            } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
-                invalidEnvs.splice(i, 1);
-                if (lastEnv) { state.push(lastEnv); } ;
-                return;
-            } else {
-                var status = reportError(lastEnv, thisEnv);
-                if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) {
-                    invalidEnvs.push(lastEnv);
-                    retry = true;
-                } else {
-                    var prevLastEnv = state.pop();
-                    if(prevLastEnv) {
-                        if (thisEnv.name === prevLastEnv.name) {
-                            return;
-                        } else {
-                            state.push(prevLastEnv);
-                        }
-                    }
-                    invalidEnvs.push(lastEnv);
-                }
-
-            }
-        } while (retry === true);
-    };
-
-    var CLOSING_DELIMITER = {
-        "{" : "}",
-        "left" : "right",
-        "[" : "]",
-        "(" : ")",
-        "$" : "$",
-        "$$": "$$"
-    };
-
-    var closedBy = function (lastEnv, thisEnv) {
-        if (!lastEnv) {
-            return false ;
-        } else if (thisEnv.command === "end") {
-            return lastEnv.command === "begin" && lastEnv.name === thisEnv.name;
-        } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) {
-            return true;
-        } else {
-            return false;
-        }
-    };
-
-    var indexOfClosingEnvInArray = function (envs, thisEnv) {
-        for (var i = 0, n = envs.length; i < n ; i++) {
-            if (closedBy(envs[i], thisEnv)) {
-                return i;
-            }
-        }
-        return -1;
-    };
-
-    var envPrecedence = function (env) {
-        var openScore = {
-            "{" : 1,
-            "left" : 2,
-            "$" : 3,
-            "$$" : 4,
-            "begin": 4
-        };
-        var closeScore = {
-            "}" : 1,
-            "right" : 2,
-            "$" : 3,
-            "$$" : 5,
-            "end": 4
-        };
-        if (env.command) {
-            return openScore[env.command] || closeScore[env.command];
-        } else {
-            return 0;
-        }
-    };
-
-    var getName = function(env) {
-        var description = {
-            "{" : "open group {",
-            "}" : "close group }",
-            "[" : "open display math \\[",
-            "]" : "close display math \\]",
-            "(" : "open inline math \\(",
-            ")" : "close inline math \\)",
-            "$" : "$",
-            "$$" : "$$",
-            "left" : "\\left",
-            "right" : "\\right"
-        };
-        if (env.command === "begin" || env.command === "end") {
-            return "\\" + env.command + "{" + env.name + "}";
-        } else if (env.command in description) {
-            return description[env.command];
-        } else {
-            return env.command;
-        }
-    };
-
-    var EXTRA_CLOSE = 1;
-    var UNCLOSED_GROUP = 2;
-    var UNCLOSED_ENV = 3;
-
-    var reportError = function(lastEnv, thisEnv) {
-        if (!lastEnv) { // unexpected close, nothing was open!
-            if (documentClosed) {
-                ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
-            } else {
-                ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
-            };
-            return EXTRA_CLOSE;
-        } else if (lastEnv.command === "{" && thisEnv.command === "end") {
-            ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
-                        {suppressIfEditing:true, errorAtStart: true, type:"warning"});
-            return UNCLOSED_GROUP;
-        } else {
-            var pLast = envPrecedence(lastEnv);
-            var pThis = envPrecedence(thisEnv);
-            if (pThis > pLast) {
-                ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
-                           {suppressIfEditing:true, errorAtStart: true});
-            } else {
-                ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv));
-            }
-            return UNCLOSED_ENV;
-        };
-    };
-
-    this._beginMathMode = function (thisEnv) {
-        var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
-        if (currentMathMode) {
-            ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
-                      {suppressIfEditing:true, errorAtStart: true, mathMode:true});
-        };
-        thisEnv.mathMode = thisEnv;
-        state.push(thisEnv);
-    };
-
-    this._toggleMathMode = function (thisEnv) {
-        var lastEnv = state.pop();
-        if (closedBy(lastEnv, thisEnv)) {
-            return;
-        } else {
-            if (lastEnv) {state.push(lastEnv);}
-            if (lastEnv && lastEnv.mathMode) {
-                this._end(thisEnv);
-            } else {
-                thisEnv.mathMode = thisEnv;
-                state.push(thisEnv);
-            }
-        };
-    };
-
-    this.getMathMode = function () {
-        var n = state.length;
-        if (n > 0) {
-            return state[n-1].mathMode;
-        } else {
-            return null;
-        }
-    };
-
-    this.insideGroup = function () {
-        var n = state.length;
-        if (n > 0) {
-            return (state[n-1].command === "{");
-        } else {
-            return null;
-        }
-    };
-
-    var resetMathMode = function () {
-        var n = state.length;
-        if (n > 0) {
-            var lastMathMode = state[n-1].mathMode;
-            do {
-                var lastEnv = state.pop();
-            } while (lastEnv && lastEnv !== lastMathMode);
-        } else {
-            return;
-        }
-    };
-
-    this.resetMathMode = resetMathMode;
-
-    var getNewMathMode = function (currentMathMode, thisEnv) {
-        var newMathMode = null;
-
-        if (thisEnv.command === "{") {
-            if (thisEnv.mathMode !== null) {
-                newMathMode = thisEnv.mathMode;
-            } else {
-                newMathMode = currentMathMode;
-            }
-        } else if (thisEnv.command === "left") {
-            if (currentMathMode === null) {
-                ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true});
-            };
-            newMathMode = currentMathMode;
-        } else if (thisEnv.command === "begin") {
-            var name = thisEnv.name;
-            if (name) {
-                if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
-                    if (currentMathMode) {
-                        ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true, mathMode: true});
-                        resetMathMode();
-                    };
-                    newMathMode = null;
-                } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) {
-                    if (currentMathMode === null) {
-                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true});
-                    };
-                    newMathMode = currentMathMode;
-                } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
-                    if (currentMathMode) {
-                        ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true, mathMode: true});
-                        resetMathMode();
-                    };
-                    newMathMode = thisEnv;
-                } else {
-                    newMathMode = undefined;  // undefined means we don't know if we are in math mode or not
-                }
-            }
-        };
-        return newMathMode;
-    };
-
-    this.checkAndUpdateState = function (thisEnv) {
-        if (inVerbatim) {
-            if (thisEnv.command === "end") {
-                this._endVerbatim(thisEnv);
-            } else {
-                return; // ignore anything in verbatim environments
-            }
-        } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") {
-            if (thisEnv.verbatim) {inVerbatim = true;};
-            var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
-            var newMathMode = getNewMathMode(currentMathMode, thisEnv);
-            thisEnv.mathMode = newMathMode;
-            state.push(thisEnv);
-        } else if (thisEnv.command === "end") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "(" || thisEnv.command === "[") {
-            this._beginMathMode(thisEnv);
-        } else if (thisEnv.command === ")" || thisEnv.command === "]") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "}") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "right") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "$" || thisEnv.command === "$$") {
-            this._toggleMathMode(thisEnv);
-        }
-    };
-
-    this.close = function () {
-        while (state.length > 0) {
-            var thisEnv = state.pop();
-            if (thisEnv.command === "{") {
-                ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
-            } else {
-                ErrorFrom(thisEnv, "unclosed " + getName(thisEnv));
-            }
-        }
-        var vlen = verbatimRanges.length;
-        var len = ErrorReporter.tokenErrors.length;
-        if (vlen >0 && len > 0) {
-            for (var i = 0; i < len; i++) {
-                var tokenError = ErrorReporter.tokenErrors[i];
-                var startPos = tokenError.startPos;
-                var endPos = tokenError.endPos;
-                for (var j = 0; j < vlen; j++) {
-                    if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
-                        tokenError.ignore = true;
-                        break;
-                    }
-                }
-            }
-        }
-    };
-
-    this.setEnvProps = function (env) {
-        var name = env.name ;
-        if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) {
-            env.verbatim = true;
-        }
-    };
-};
-var ErrorReporter = function (TokeniseResult) {
-    var text = TokeniseResult.text;
-    var linePosition = TokeniseResult.linePosition;
-    var lineNumber = TokeniseResult.lineNumber;
-
-    var errors = [], tokenErrors = [];
-    this.errors = errors;
-    this.tokenErrors = tokenErrors;
-    this.filterMath = false;
-
-    this.getErrors = function () {
-        var returnedErrors = [];
-        for (var i = 0, len = tokenErrors.length; i < len; i++) {
-            if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
-        }
-        var allErrors = returnedErrors.concat(errors);
-        var result = [];
-
-        var mathErrorCount = 0;
-        for (i = 0, len = allErrors.length; i < len; i++) {
-            if (allErrors[i].mathMode) {
-                mathErrorCount++;
-            }
-            if (mathErrorCount > 10) {
-                return [];
-            }
-        }
-
-        if (this.filterMath && mathErrorCount > 0) {
-            for (i = 0, len = allErrors.length; i < len; i++) {
-                if (!allErrors[i].mathMode) {
-                    result.push(allErrors[i]);
-                }
-            }
-            return result;
-        } else {
-            return allErrors;
-        }
-    };
-
-    this.TokenError = function (token, message, options) {
-        if(!options) { options = { suppressIfEditing:true } ; };
-        var line = token[0], type = token[1], start = token[2], end = token[3];
-        var start_col = start - linePosition[line];
-        if (!end) { end = start + 1; } ;
-        var end_col = end - linePosition[line];
-        tokenErrors.push({row: line,
-                          column: start_col,
-                          start_row:line,
-                          start_col: start_col,
-                          end_row:line,
-                          end_col: end_col,
-                          type:"error",
-                          text:message,
-                          startPos: start,
-                          endPos: end,
-                          suppressIfEditing:options.suppressIfEditing,
-                          mathMode: options.mathMode});
-    };
-
-    this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
-        if(!options) { options = {suppressIfEditing:true } ; };
-        var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
-        var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
-        if (!toEnd) { toEnd = toStart + 1;};
-        var start_col = fromStart - linePosition[fromLine];
-        var end_col = toEnd - linePosition[toLine];
-
-        tokenErrors.push({row: fromLine,
-                          column: start_col,
-                          start_row: fromLine,
-                          start_col: start_col,
-                          end_row: toLine,
-                          end_col: end_col,
-                          type:"error",
-                          text:message,
-                          startPos: fromStart,
-                          endPos: toEnd,
-                          suppressIfEditing:options.suppressIfEditing,
-                          mathMode: options.mathMode});
-    };
-
-
-    this.EnvErrorFromTo = function (fromEnv, toEnv, message, options) {
-        if(!options) { options = {} ; };
-        var fromToken = fromEnv.token, toToken = toEnv.closeToken || toEnv.token;
-        var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
-        if (!toToken) {toToken = fromToken;};
-        var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
-        if (!toEnd) { toEnd = toStart + 1;};
-        var start_col = fromStart - linePosition[fromLine];
-        var end_col = toEnd - linePosition[toLine];
-        errors.push({row: options.errorAtStart ? fromLine : toLine,
-                     column: options.errorAtStart ? start_col: end_col,
-                     start_row:fromLine,
-                     start_col: start_col,
-                     end_row:toLine,
-                     end_col: end_col,
-                     type: options.type ? options.type : "error",
-                     text:message,
-                     suppressIfEditing:options.suppressIfEditing,
-                     mathMode: options.mathMode});
-    };
-
-    this.EnvErrorTo = function (toEnv, message, options) {
-        if(!options) { options = {} ; };
-        var token = toEnv.closeToken || toEnv.token;
-        var line = token[0], type = token[1], start = token[2], end = token[3];
-        if (!end) { end = start + 1; };
-        var end_col = end - linePosition[line];
-        var err = {row: line,
-                   column: end_col,
-                   start_row:0,
-                   start_col: 0,
-                   end_row: line,
-                   end_col: end_col,
-                   type: options.type ? options.type : "error",
-                   text:message,
-                   mathMode: options.mathMode};
-        errors.push(err);
-    };
-
-    this.EnvErrorFrom = function (env, message, options) {
-        if(!options) { options = {} ; };
-        var token = env.token;
-        var line = token[0], type = token[1], start = token[2], end = token[3];
-        var start_col = start - linePosition[line];
-        var end_col = Infinity;
-        errors.push({row: line,
-                     column: start_col,
-                     start_row:line,
-                     start_col: start_col,
-                     end_row: lineNumber,
-                     end_col: end_col,
-                     type: options.type ? options.type : "error",
-                     text:message,
-                     mathMode: options.mathMode});
-    };
-};
-
-var Parse = function (text) {
-    var TokeniseResult = Tokenise(text);
-    var Reporter = new ErrorReporter(TokeniseResult);
-    var Environments = InterpretTokens(TokeniseResult, Reporter);
-    Environments.close();
-    return Reporter.getErrors();
-};
-
-(function() {
-    var disabled = false;
-
-    this.onUpdate = function() {
-        if (disabled) { return ; };
-
-        var value = this.doc.getValue();
-        var errors = [];
-        try {
-            if (value)
-                errors = Parse(value);
-        } catch (e) {
-            disabled = true;
-            errors = [];
-        }
-        this.sender.emit("lint", errors);
-    };
-
-}).call(LatexWorker.prototype);
-
-});
-
-ace.define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) {
-
-function Empty() {}
-
-if (!Function.prototype.bind) {
-    Function.prototype.bind = function bind(that) { // .length is 1
-        var target = this;
-        if (typeof target != "function") {
-            throw new TypeError("Function.prototype.bind called on incompatible " + target);
-        }
-        var args = slice.call(arguments, 1); // for normal call
-        var bound = function () {
-
-            if (this instanceof bound) {
-
-                var result = target.apply(
-                    this,
-                    args.concat(slice.call(arguments))
-                );
-                if (Object(result) === result) {
-                    return result;
-                }
-                return this;
-
-            } else {
-                return target.apply(
-                    that,
-                    args.concat(slice.call(arguments))
-                );
-
-            }
-
-        };
-        if(target.prototype) {
-            Empty.prototype = target.prototype;
-            bound.prototype = new Empty();
-            Empty.prototype = null;
-        }
-        return bound;
-    };
-}
-var call = Function.prototype.call;
-var prototypeOfArray = Array.prototype;
-var prototypeOfObject = Object.prototype;
-var slice = prototypeOfArray.slice;
-var _toString = call.bind(prototypeOfObject.toString);
-var owns = call.bind(prototypeOfObject.hasOwnProperty);
-var defineGetter;
-var defineSetter;
-var lookupGetter;
-var lookupSetter;
-var supportsAccessors;
-if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) {
-    defineGetter = call.bind(prototypeOfObject.__defineGetter__);
-    defineSetter = call.bind(prototypeOfObject.__defineSetter__);
-    lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);
-    lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);
-}
-if ([1,2].splice(0).length != 2) {
-    if(function() { // test IE < 9 to splice bug - see issue #138
-        function makeArray(l) {
-            var a = new Array(l+2);
-            a[0] = a[1] = 0;
-            return a;
-        }
-        var array = [], lengthBefore;
-        
-        array.splice.apply(array, makeArray(20));
-        array.splice.apply(array, makeArray(26));
-
-        lengthBefore = array.length; //46
-        array.splice(5, 0, "XXX"); // add one element
-
-        lengthBefore + 1 == array.length
-
-        if (lengthBefore + 1 == array.length) {
-            return true;// has right splice implementation without bugs
-        }
-    }()) {//IE 6/7
-        var array_splice = Array.prototype.splice;
-        Array.prototype.splice = function(start, deleteCount) {
-            if (!arguments.length) {
-                return [];
-            } else {
-                return array_splice.apply(this, [
-                    start === void 0 ? 0 : start,
-                    deleteCount === void 0 ? (this.length - start) : deleteCount
-                ].concat(slice.call(arguments, 2)))
-            }
-        };
-    } else {//IE8
-        Array.prototype.splice = function(pos, removeCount){
-            var length = this.length;
-            if (pos > 0) {
-                if (pos > length)
-                    pos = length;
-            } else if (pos == void 0) {
-                pos = 0;
-            } else if (pos < 0) {
-                pos = Math.max(length + pos, 0);
-            }
-
-            if (!(pos+removeCount < length))
-                removeCount = length - pos;
-
-            var removed = this.slice(pos, pos+removeCount);
-            var insert = slice.call(arguments, 2);
-            var add = insert.length;            
-            if (pos === length) {
-                if (add) {
-                    this.push.apply(this, insert);
-                }
-            } else {
-                var remove = Math.min(removeCount, length - pos);
-                var tailOldPos = pos + remove;
-                var tailNewPos = tailOldPos + add - remove;
-                var tailCount = length - tailOldPos;
-                var lengthAfterRemove = length - remove;
-
-                if (tailNewPos < tailOldPos) { // case A
-                    for (var i = 0; i < tailCount; ++i) {
-                        this[tailNewPos+i] = this[tailOldPos+i];
-                    }
-                } else if (tailNewPos > tailOldPos) { // case B
-                    for (i = tailCount; i--; ) {
-                        this[tailNewPos+i] = this[tailOldPos+i];
-                    }
-                } // else, add == remove (nothing to do)
-
-                if (add && pos === lengthAfterRemove) {
-                    this.length = lengthAfterRemove; // truncate array
-                    this.push.apply(this, insert);
-                } else {
-                    this.length = lengthAfterRemove + add; // reserves space
-                    for (i = 0; i < add; ++i) {
-                        this[pos+i] = insert[i];
-                    }
-                }
-            }
-            return removed;
-        };
-    }
-}
-if (!Array.isArray) {
-    Array.isArray = function isArray(obj) {
-        return _toString(obj) == "[object Array]";
-    };
-}
-var boxedString = Object("a"),
-    splitString = boxedString[0] != "a" || !(0 in boxedString);
-
-if (!Array.prototype.forEach) {
-    Array.prototype.forEach = function forEach(fun /*, thisp*/) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                object,
-            thisp = arguments[1],
-            i = -1,
-            length = self.length >>> 0;
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(); // TODO message
-        }
-
-        while (++i < length) {
-            if (i in self) {
-                fun.call(thisp, self[i], i, object);
-            }
-        }
-    };
-}
-if (!Array.prototype.map) {
-    Array.prototype.map = function map(fun /*, thisp*/) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                object,
-            length = self.length >>> 0,
-            result = Array(length),
-            thisp = arguments[1];
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(fun + " is not a function");
-        }
-
-        for (var i = 0; i < length; i++) {
-            if (i in self)
-                result[i] = fun.call(thisp, self[i], i, object);
-        }
-        return result;
-    };
-}
-if (!Array.prototype.filter) {
-    Array.prototype.filter = function filter(fun /*, thisp */) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                    object,
-            length = self.length >>> 0,
-            result = [],
-            value,
-            thisp = arguments[1];
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(fun + " is not a function");
-        }
-
-        for (var i = 0; i < length; i++) {
-            if (i in self) {
-                value = self[i];
-                if (fun.call(thisp, value, i, object)) {
-                    result.push(value);
-                }
-            }
-        }
-        return result;
-    };
-}
-if (!Array.prototype.every) {
-    Array.prototype.every = function every(fun /*, thisp */) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                object,
-            length = self.length >>> 0,
-            thisp = arguments[1];
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(fun + " is not a function");
-        }
-
-        for (var i = 0; i < length; i++) {
-            if (i in self && !fun.call(thisp, self[i], i, object)) {
-                return false;
-            }
-        }
-        return true;
-    };
-}
-if (!Array.prototype.some) {
-    Array.prototype.some = function some(fun /*, thisp */) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                object,
-            length = self.length >>> 0,
-            thisp = arguments[1];
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(fun + " is not a function");
-        }
-
-        for (var i = 0; i < length; i++) {
-            if (i in self && fun.call(thisp, self[i], i, object)) {
-                return true;
-            }
-        }
-        return false;
-    };
-}
-if (!Array.prototype.reduce) {
-    Array.prototype.reduce = function reduce(fun /*, initial*/) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                object,
-            length = self.length >>> 0;
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(fun + " is not a function");
-        }
-        if (!length && arguments.length == 1) {
-            throw new TypeError("reduce of empty array with no initial value");
-        }
-
-        var i = 0;
-        var result;
-        if (arguments.length >= 2) {
-            result = arguments[1];
-        } else {
-            do {
-                if (i in self) {
-                    result = self[i++];
-                    break;
-                }
-                if (++i >= length) {
-                    throw new TypeError("reduce of empty array with no initial value");
-                }
-            } while (true);
-        }
-
-        for (; i < length; i++) {
-            if (i in self) {
-                result = fun.call(void 0, result, self[i], i, object);
-            }
-        }
-
-        return result;
-    };
-}
-if (!Array.prototype.reduceRight) {
-    Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {
-        var object = toObject(this),
-            self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                object,
-            length = self.length >>> 0;
-        if (_toString(fun) != "[object Function]") {
-            throw new TypeError(fun + " is not a function");
-        }
-        if (!length && arguments.length == 1) {
-            throw new TypeError("reduceRight of empty array with no initial value");
-        }
-
-        var result, i = length - 1;
-        if (arguments.length >= 2) {
-            result = arguments[1];
-        } else {
-            do {
-                if (i in self) {
-                    result = self[i--];
-                    break;
-                }
-                if (--i < 0) {
-                    throw new TypeError("reduceRight of empty array with no initial value");
-                }
-            } while (true);
-        }
-
-        do {
-            if (i in this) {
-                result = fun.call(void 0, result, self[i], i, object);
-            }
-        } while (i--);
-
-        return result;
-    };
-}
-if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) {
-    Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) {
-        var self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                toObject(this),
-            length = self.length >>> 0;
-
-        if (!length) {
-            return -1;
-        }
-
-        var i = 0;
-        if (arguments.length > 1) {
-            i = toInteger(arguments[1]);
-        }
-        i = i >= 0 ? i : Math.max(0, length + i);
-        for (; i < length; i++) {
-            if (i in self && self[i] === sought) {
-                return i;
-            }
-        }
-        return -1;
-    };
-}
-if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) {
-    Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) {
-        var self = splitString && _toString(this) == "[object String]" ?
-                this.split("") :
-                toObject(this),
-            length = self.length >>> 0;
-
-        if (!length) {
-            return -1;
-        }
-        var i = length - 1;
-        if (arguments.length > 1) {
-            i = Math.min(i, toInteger(arguments[1]));
-        }
-        i = i >= 0 ? i : length - Math.abs(i);
-        for (; i >= 0; i--) {
-            if (i in self && sought === self[i]) {
-                return i;
-            }
-        }
-        return -1;
-    };
-}
-if (!Object.getPrototypeOf) {
-    Object.getPrototypeOf = function getPrototypeOf(object) {
-        return object.__proto__ || (
-            object.constructor ?
-            object.constructor.prototype :
-            prototypeOfObject
-        );
-    };
-}
-if (!Object.getOwnPropertyDescriptor) {
-    var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " +
-                         "non-object: ";
-    Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {
-        if ((typeof object != "object" && typeof object != "function") || object === null)
-            throw new TypeError(ERR_NON_OBJECT + object);
-        if (!owns(object, property))
-            return;
-
-        var descriptor, getter, setter;
-        descriptor =  { enumerable: true, configurable: true };
-        if (supportsAccessors) {
-            var prototype = object.__proto__;
-            object.__proto__ = prototypeOfObject;
-
-            var getter = lookupGetter(object, property);
-            var setter = lookupSetter(object, property);
-            object.__proto__ = prototype;
-
-            if (getter || setter) {
-                if (getter) descriptor.get = getter;
-                if (setter) descriptor.set = setter;
-                return descriptor;
-            }
-        }
-        descriptor.value = object[property];
-        return descriptor;
-    };
-}
-if (!Object.getOwnPropertyNames) {
-    Object.getOwnPropertyNames = function getOwnPropertyNames(object) {
-        return Object.keys(object);
-    };
-}
-if (!Object.create) {
-    var createEmpty;
-    if (Object.prototype.__proto__ === null) {
-        createEmpty = function () {
-            return { "__proto__": null };
-        };
-    } else {
-        createEmpty = function () {
-            var empty = {};
-            for (var i in empty)
-                empty[i] = null;
-            empty.constructor =
-            empty.hasOwnProperty =
-            empty.propertyIsEnumerable =
-            empty.isPrototypeOf =
-            empty.toLocaleString =
-            empty.toString =
-            empty.valueOf =
-            empty.__proto__ = null;
-            return empty;
-        }
-    }
-
-    Object.create = function create(prototype, properties) {
-        var object;
-        if (prototype === null) {
-            object = createEmpty();
-        } else {
-            if (typeof prototype != "object")
-                throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'");
-            var Type = function () {};
-            Type.prototype = prototype;
-            object = new Type();
-            object.__proto__ = prototype;
-        }
-        if (properties !== void 0)
-            Object.defineProperties(object, properties);
-        return object;
-    };
-}
-
-function doesDefinePropertyWork(object) {
-    try {
-        Object.defineProperty(object, "sentinel", {});
-        return "sentinel" in object;
-    } catch (exception) {
-    }
-}
-if (Object.defineProperty) {
-    var definePropertyWorksOnObject = doesDefinePropertyWork({});
-    var definePropertyWorksOnDom = typeof document == "undefined" ||
-        doesDefinePropertyWork(document.createElement("div"));
-    if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) {
-        var definePropertyFallback = Object.defineProperty;
-    }
-}
-
-if (!Object.defineProperty || definePropertyFallback) {
-    var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ";
-    var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: "
-    var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " +
-                                      "on this javascript engine";
-
-    Object.defineProperty = function defineProperty(object, property, descriptor) {
-        if ((typeof object != "object" && typeof object != "function") || object === null)
-            throw new TypeError(ERR_NON_OBJECT_TARGET + object);
-        if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null)
-            throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
-        if (definePropertyFallback) {
-            try {
-                return definePropertyFallback.call(Object, object, property, descriptor);
-            } catch (exception) {
-            }
-        }
-        if (owns(descriptor, "value")) {
-
-            if (supportsAccessors && (lookupGetter(object, property) ||
-                                      lookupSetter(object, property)))
-            {
-                var prototype = object.__proto__;
-                object.__proto__ = prototypeOfObject;
-                delete object[property];
-                object[property] = descriptor.value;
-                object.__proto__ = prototype;
-            } else {
-                object[property] = descriptor.value;
-            }
-        } else {
-            if (!supportsAccessors)
-                throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
-            if (owns(descriptor, "get"))
-                defineGetter(object, property, descriptor.get);
-            if (owns(descriptor, "set"))
-                defineSetter(object, property, descriptor.set);
-        }
-
-        return object;
-    };
-}
-if (!Object.defineProperties) {
-    Object.defineProperties = function defineProperties(object, properties) {
-        for (var property in properties) {
-            if (owns(properties, property))
-                Object.defineProperty(object, property, properties[property]);
-        }
-        return object;
-    };
-}
-if (!Object.seal) {
-    Object.seal = function seal(object) {
-        return object;
-    };
-}
-if (!Object.freeze) {
-    Object.freeze = function freeze(object) {
-        return object;
-    };
-}
-try {
-    Object.freeze(function () {});
-} catch (exception) {
-    Object.freeze = (function freeze(freezeObject) {
-        return function freeze(object) {
-            if (typeof object == "function") {
-                return object;
-            } else {
-                return freezeObject(object);
-            }
-        };
-    })(Object.freeze);
-}
-if (!Object.preventExtensions) {
-    Object.preventExtensions = function preventExtensions(object) {
-        return object;
-    };
-}
-if (!Object.isSealed) {
-    Object.isSealed = function isSealed(object) {
-        return false;
-    };
-}
-if (!Object.isFrozen) {
-    Object.isFrozen = function isFrozen(object) {
-        return false;
-    };
-}
-if (!Object.isExtensible) {
-    Object.isExtensible = function isExtensible(object) {
-        if (Object(object) === object) {
-            throw new TypeError(); // TODO message
-        }
-        var name = '';
-        while (owns(object, name)) {
-            name += '?';
-        }
-        object[name] = true;
-        var returnValue = owns(object, name);
-        delete object[name];
-        return returnValue;
-    };
-}
-if (!Object.keys) {
-    var hasDontEnumBug = true,
-        dontEnums = [
-            "toString",
-            "toLocaleString",
-            "valueOf",
-            "hasOwnProperty",
-            "isPrototypeOf",
-            "propertyIsEnumerable",
-            "constructor"
-        ],
-        dontEnumsLength = dontEnums.length;
-
-    for (var key in {"toString": null}) {
-        hasDontEnumBug = false;
-    }
-
-    Object.keys = function keys(object) {
-
-        if (
-            (typeof object != "object" && typeof object != "function") ||
-            object === null
-        ) {
-            throw new TypeError("Object.keys called on a non-object");
-        }
-
-        var keys = [];
-        for (var name in object) {
-            if (owns(object, name)) {
-                keys.push(name);
-            }
-        }
-
-        if (hasDontEnumBug) {
-            for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
-                var dontEnum = dontEnums[i];
-                if (owns(object, dontEnum)) {
-                    keys.push(dontEnum);
-                }
-            }
-        }
-        return keys;
-    };
-
-}
-if (!Date.now) {
-    Date.now = function now() {
-        return new Date().getTime();
-    };
-}
-var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
-    "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
-    "\u2029\uFEFF";
-if (!String.prototype.trim || ws.trim()) {
-    ws = "[" + ws + "]";
-    var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
-        trimEndRegexp = new RegExp(ws + ws + "*$");
-    String.prototype.trim = function trim() {
-        return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, "");
-    };
-}
-
-function toInteger(n) {
-    n = +n;
-    if (n !== n) { // isNaN
-        n = 0;
-    } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) {
-        n = (n > 0 || -1) * Math.floor(Math.abs(n));
-    }
-    return n;
-}
-
-function isPrimitive(input) {
-    var type = typeof input;
-    return (
-        input === null ||
-        type === "undefined" ||
-        type === "boolean" ||
-        type === "number" ||
-        type === "string"
-    );
-}
-
-function toPrimitive(input) {
-    var val, valueOf, toString;
-    if (isPrimitive(input)) {
-        return input;
-    }
-    valueOf = input.valueOf;
-    if (typeof valueOf === "function") {
-        val = valueOf.call(input);
-        if (isPrimitive(val)) {
-            return val;
-        }
-    }
-    toString = input.toString;
-    if (typeof toString === "function") {
-        val = toString.call(input);
-        if (isPrimitive(val)) {
-            return val;
-        }
-    }
-    throw new TypeError();
-}
-var toObject = function (o) {
-    if (o == null) { // this matches both null and undefined
-        throw new TypeError("can't convert "+o+" to object");
-    }
-    return Object(o);
-};
-
-});

BIN
services/web/public/ol-favicon.ico


+ 84 - 0
services/web/public/stylesheets/_style_includes.less

@@ -0,0 +1,84 @@
+@import url(https://netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css);
+
+@import "core/mixins.less";
+
+// Reset
+@import "core/normalize.less";
+@import "core/print.less";
+
+// Core CSS
+@import "core/scaffolding.less";
+@import "core/type.less";
+@import "core/grid.less";
+
+// Components
+@import "components/tables.less";
+@import "components/forms.less";
+@import "components/buttons.less";
+@import "components/card.less";
+//@import "components/code.less";
+@import "components/component-animations.less";
+@import "components/glyphicons.less";
+@import "components/dropdowns.less";
+@import "components/button-groups.less";
+@import "components/input-groups.less";
+@import "components/navs.less";
+@import "components/navbar.less";
+@import "components/footer.less";
+//@import "components/breadcrumbs.less";
+//@import "components/pagination.less";
+@import "components/pager.less";
+@import "components/labels.less";
+//@import "components/badges.less";
+//@import "components/jumbotron.less";
+@import "components/thumbnails.less";
+@import "components/alerts.less";
+@import "components/progress-bars.less";
+// @import "components/media.less";
+// @import "components/list-group.less";
+// @import "components/panels.less";
+// @import "components/wells.less";
+@import "components/close.less";
+@import "components/fineupload.less";
+@import "components/hover.less";
+
+// Components w/ JavaScript
+@import "components/modals.less";
+@import "components/tooltip.less";
+@import "components/popovers.less";
+@import "components/carousel.less";
+
+// ngTagsInput
+@import "components/tags-input.less";
+
+// Utility classes
+@import "core/utilities.less";
+@import "core/responsive-utilities.less";
+
+// ShareLaTeX app classes
+@import "app/base.less";
+@import "app/account-settings.less";
+@import "app/beta-program.less";
+@import "app/about-page.less";
+@import "app/project-list.less";
+@import "app/editor.less";
+@import "app/homepage.less";
+@import "app/plans.less";
+@import "app/recurly.less";
+@import "app/bonus.less";
+@import "app/register.less";
+@import "app/blog.less";
+@import "app/features.less";
+@import "app/templates.less";
+@import "app/wiki.less";
+@import "app/translations.less";
+@import "app/contact-us.less";
+@import "app/subscription.less";
+@import "app/sprites.less";
+@import "app/invite.less";
+@import "app/review-features-page.less";
+@import "app/error-pages.less";
+
+@import	"../js/libs/pdfListView/TextLayer.css";
+@import	"../js/libs/pdfListView/AnnotationsLayer.css";
+@import	"../js/libs/pdfListView/HighlightsLayer.css";

+ 6 - 5
services/web/public/stylesheets/app/editor.less

@@ -72,17 +72,17 @@
 	height: 100%;
 	background-color: #FFF;
 }
-	.loading-screen-lion-container {
+	.loading-screen-brand-container {
 		width: 15%;
 		min-width: 200px;
 		text-align: center;
 	}
-		.loading-screen-lion {
+		.loading-screen-brand {
 			position: relative;
 			width: 100%;
-			padding-top: 86.2%;
+			padding-top: @editor-loading-logo-padding-top;
 			height: 0;
-			background: url(/img/brand/lion-grey.svg) no-repeat bottom / 100%;
+			background: @editor-loading-logo-background-url no-repeat bottom / 100%;
 
     		&::after {
 				content: '';
@@ -91,7 +91,7 @@
 				right: 0;
 				bottom: 0;
 				left: 0;
-				background: url(/img/brand/lion.svg) no-repeat bottom / 100%;
+				background: @editor-loading-logo-foreground-url no-repeat bottom / 100%;
 				transition: height .5s;
     		}
 		}
@@ -504,3 +504,4 @@
 	height: auto;
 	border-bottom: 1px solid @modal-header-border-color;
 }
+

+ 58 - 9
services/web/public/stylesheets/app/editor/review-panel.less

@@ -57,6 +57,15 @@
 	}
 }
 
+.rp-collapse-arrow() {
+	display: inline-block;
+	transform: rotateZ(0deg);
+	transition: transform 0.15s ease;
+	&-on {
+		transform: rotateZ(-90deg);
+	}
+}
+
 .triangle(@_, @width, @height, @color) {
 	position: absolute;
 	border-color: transparent;
@@ -131,7 +140,6 @@
 	}
 
 	position: relative;
-	height: @rp-toolbar-height;
 	border-bottom: 1px solid @rp-border-grey;
 	background-color: @rp-bg-dim-blue;
 	text-align: center;
@@ -144,6 +152,10 @@
 		text-align: right;
 		flex-grow: 1;
 	}
+		.review-panel-toolbar-icon-on {
+			margin-right: 5px;
+			color: @red;
+		}
 		.review-panel-toolbar-label-disabled {
 			cursor: auto;
 			margin-right: 5px;
@@ -151,6 +163,45 @@
 	.review-panel-toolbar-spinner {
 		margin-left: 5px;
 	}
+	.rp-tc-state {
+		position: absolute;
+		top: 100%;
+		left: 0;
+		right: 0;
+		overflow: hidden;
+		list-style: none;
+		padding: 0 5px;
+		margin: 0;
+		border-bottom: 1px solid @rp-border-grey;
+		background-color: @rp-bg-dim-blue;
+		text-align: left;
+	}
+		.rp-tc-state-collapse {
+			.rp-collapse-arrow;
+			margin-left: 5px;
+		}
+		.rp-tc-state-item {
+			display: flex;
+			align-items: center;
+			padding: 3px 0;	
+			&:last-of-type {
+				padding-bottom: 5px;
+			}
+		}
+		.rp-tc-state-item-everyone {
+			border-bottom: 1px solid @rp-border-grey;
+			color: @red;
+		}
+			.rp-tc-state-item-name {
+				flex-grow: 1;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				white-space: nowrap;
+				font-weight: @rp-semibold-weight;
+			}
+			.rp-tc-state-item-name-disabled {
+				opacity: .35;
+			}
 
 .rp-entry-list {
 	display: none;
@@ -629,16 +680,9 @@
 		font-size: 0.9em;
 	}
 	.rp-overview-file-header-collapse {
-		display: inline-block;
+		.rp-collapse-arrow;
 		float: left;
-		transform: rotateZ(0deg);
-		transition: transform 0.15s ease
 	}
-	
-	.rp-overview-file-header-collapse-on {
-		transform: rotateZ(-90deg);
-	}
-
 	.rp-overview-file-entries {
 		overflow: hidden;
 	}
@@ -771,6 +815,11 @@
 				background-color: #FFF;
 			}
 		}
+
+		&:disabled + .rp-toggle-btn {
+			cursor: default;
+			opacity: .35;
+		}
 	}
 
 .ace-editor-wrapper {

+ 1 - 1
services/web/public/stylesheets/components/navbar.less

@@ -386,7 +386,7 @@
     bottom: 5px;
     width: 180px;
     padding: 0;
-    background-image: url('/img/brand/logo-horizontal.svg');
+    background-image: @navbar-brand-image-url;
     background-size: contain;
     background-repeat: no-repeat;
     background-position: left center;

+ 4 - 0
services/web/public/stylesheets/components/navs.less

@@ -225,6 +225,10 @@
 
 // Hide tabbable panes to start, show them when `.active`
 .tab-content {
+  background-color: @nav-tabs-active-link-hover-bg;
+  border: 1px solid @nav-tabs-border-color;
+  border-top: none;
+  padding: @line-height-computed / 2;
   > .tab-pane {
     display: none;
   }

+ 805 - 0
services/web/public/stylesheets/core/_common-variables.less

@@ -0,0 +1,805 @@
+//
+// Variables
+// --------------------------------------------------
+
+//== Scaffolding
+//
+// ## Settings for some of the most global styles.
+
+//** Background color for `<body>`.
+@body-bg:               #fff;
+//** Global text color on `<body>`.
+@text-color:            @gray-dark;
+
+//** Global textual link color.
+@link-color:            @brand-primary;
+//** Link hover color set via `darken()` function.
+@link-hover-color:      darken(@link-color, 15%);
+
+
+//== Typography
+//
+//## Font, line-height, and color for body text, headings, and more.
+
+@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700);
+//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i);
+@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
+
+@font-family-sans-serif:  "Open Sans", sans-serif;
+@font-family-serif:       "Merriweather", serif;
+//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          16px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            floor((@font-size-base * 2)); // ~36px
+@font-size-h2:            floor((@font-size-base * 1.6)); // ~30px
+@font-size-h3:            ceil((@font-size-base * 1.25)); // ~24px
+@font-size-h4:            ceil((@font-size-base * 1.1)); // ~18px
+@font-size-h5:            @font-size-base;
+@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.5625; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the `<body>`.
+@headings-font-family:    @font-family-serif;
+@headings-font-weight:    500;
+@headings-line-height:    1.1;
+@headings-color:          @gray-dark;
+
+
+//-- Iconography
+//
+//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+@icon-font-path:          "../fonts/";
+@icon-font-name:          "glyphicons-halflings-regular";
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     5px;
+@padding-base-horizontal:   16px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.33;
+@line-height-small:         1.5;
+
+@border-radius-base:        3px;
+@border-radius-large:       5px;
+@border-radius-small:       2px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for `<th>`s and `<td>`s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                #f5f5f5;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                700;
+
+@btn-default-color:              #333;
+@btn-default-bg:                 #fff;
+@btn-default-border:             #ccc;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             darken(@btn-primary-bg, 10%);
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             darken(@btn-success-bg, 10%);
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                darken(@btn-info-bg, 15%);
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             darken(@btn-warning-bg, 10%);
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              darken(@btn-danger-bg, 10%);
+
+@btn-link-disabled-color:        @gray-light;
+
+
+//== Forms
+//
+//##
+
+//** `<input>` background color
+@input-bg:                       #fff;
+//** `<input disabled>` background color
+@input-bg-disabled:              @gray-lighter;
+
+//** Text color for `<input>`s
+@input-color:                    @gray;
+//** `<input>` border color
+@input-border:                   #ccc;
+//** `<input>` border radius
+@input-border-radius:            @border-radius-base;
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        @gray-light;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           @gray-lighter;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @gray-dark;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      #fff;
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         @brand-primary;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+// Note: Deprecated @dropdown-caret-color as of v3.1.0
+@dropdown-caret-color:           #000;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1010;
+@zindex-tooltip:           1030;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
+@screen-xs:                  480px;
+@screen-xs-min:              @screen-xs;
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             ((720px + @grid-gutter-width));
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            ((940px + @grid-gutter-width));
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      ((1140px + @grid-gutter-width));
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    60px;
+@navbar-margin-bottom:             0;
+@navbar-border-radius:             0;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             #777;
+@navbar-default-bg:                #fff;
+@navbar-default-border:            @gray-lighter;
+
+// Navbar links
+@navbar-default-link-color:                @link-color;
+@navbar-default-link-hover-color:          @link-hover-color;
+@navbar-default-link-hover-bg:             @link-hover-color;
+@navbar-default-link-active-color:         #fff;
+@navbar-default-link-active-bg:            @link-hover-color;
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           @link-hover-color;
+@navbar-default-toggle-border-color:       @link-color;
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @link-color;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+@nav-open-link-hover-color:                 #fff;
+
+//== Tabs
+@nav-tabs-border-color:                     #ddd;
+
+@nav-tabs-link-hover-border-color:          @link-color;
+
+@nav-tabs-active-link-hover-bg:             @body-bg;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   #ddd;
+
+@nav-tabs-justified-link-border-color:            #ddd;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   @gray-lighter;
+@jumbotron-heading-color:        inherit;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             darken(@brand-success, 20%);
+@state-success-bg:               lighten(@brand-success, 50%);
+@state-success-border:           darken(@brand-success, 5%);
+
+@state-info-text:                darken(@brand-info, 20%);
+@state-info-bg:                  lighten(@brand-info, 47%);
+@state-info-border:              darken(@brand-info, 7%);
+
+@state-warning-text:             darken(@brand-warning, 10%);
+@state-warning-bg:               lighten(@brand-warning, 45%);
+@state-warning-border:           @brand-warning;
+
+@state-danger-text:              darken(@brand-danger, 10%);
+@state-danger-bg:                lighten(@brand-danger, 50%);
+@state-danger-border:            darken(@brand-danger, 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #000;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                rgba(0,0,0,.2);
+//** Popover fallback border color
+@popover-fallback-border-color:       #ccc;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 #fff;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         20px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   rgba(0,0,0,.2);
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   #e5e5e5;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+@modal-footer-background-color:   @gray-lightest;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 white;
+@progress-border-color:       @gray-lighter;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list elements on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list elements
+@list-group-active-color:       @component-active-color;
+//** Background color of active list elements
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+@list-group-link-color:         #555;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          @state-success-text;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @state-success-bg;
+
+@panel-info-text:             @state-info-text;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @state-info-bg;
+
+@panel-warning-text:          @state-warning-text;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @state-warning-bg;
+
+@panel-danger-text:           @state-danger-text;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @state-danger-bg;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f5f5f5;
+@well-border:                 darken(@well-bg, 7%);
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           bold;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           bold;
+@close-color:                 #000;
+@close-text-shadow:           0 1px 0 #fff;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.125);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+
+
+//== Miscellaneous
+//
+//##
+
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+
+@content-margin-top: @line-height-computed;
+@content-margin-top: @line-height-computed;
+
+// Custom
+
+@left-menu-width: 260px;
+@left-menu-animation-duration: 0.35s;
+
+@toolbar-border-color: @gray-lighter;
+@file-tree-droppable-background-color: rgb(252, 231, 199);
+
+@editor-dark-background-color: #333;
+@editor-dark-toolbar-border-color: #222;
+@editor-dark-highlight-color: #FFA03A;

+ 37 - 0
services/web/public/stylesheets/core/ol-variables.less

@@ -0,0 +1,37 @@
+@ol-green:      #4A9F48;
+@ol-dark-green: #1C5B26;
+
+//== Colors
+//
+//## Gray and brand colors for use across Bootstrap.
+
+
+@gray-darker:           #252525;
+@gray-dark:             #505050;
+@gray:                  #7a7a7a;
+@gray-light:            #a4a4a4;
+@gray-lighter:          #cfcfcf;
+@gray-lightest:         #f0f0f0;
+
+@blue:                  #405ebf;
+@blueDark:              #040D2D;
+@green:                 #46a546;
+@red:                   #a93529;
+@yellow:                #A1A729;
+@orange:                #f89406;
+@pink:                  #c3325f;
+@purple:                #7a43b6;
+
+@brand-primary:         @ol-green;
+@brand-success:         @green;
+@brand-info:            @ol-dark-green;
+@brand-warning:         @orange;
+@brand-danger:          #E03A06;
+
+@navbar-brand-image-url: url(/img/ol-brand/logo-horizontal.png);
+
+@editor-loading-logo-padding-top: 115.44%;
+@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);
+@editor-loading-logo-foreground-url: url(/img/ol-brand/overleaf-o.svg);
+
+@import "./_common-variables.less";

+ 5 - 804
services/web/public/stylesheets/core/variables.less

@@ -1,8 +1,3 @@
-//
-// Variables
-// --------------------------------------------------
-
-
 //== Colors
 //
 //## Gray and brand colors for use across Bootstrap.
@@ -30,804 +25,10 @@
 @brand-warning:         @orange;
 @brand-danger:          #E03A06;
 
-//== Scaffolding
-//
-// ## Settings for some of the most global styles.
-
-//** Background color for `<body>`.
-@body-bg:               #fff;
-//** Global text color on `<body>`.
-@text-color:            @gray-dark;
-
-//** Global textual link color.
-@link-color:            @brand-primary;
-//** Link hover color set via `darken()` function.
-@link-hover-color:      darken(@link-color, 15%);
-
-
-//== Typography
-//
-//## Font, line-height, and color for body text, headings, and more.
-
-@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
-//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700);
-//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i);
-@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
-
-@font-family-sans-serif:  "Open Sans", sans-serif;
-@font-family-serif:       "Merriweather", serif;
-//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
-@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
-@font-family-base:        @font-family-sans-serif;
-
-@font-size-base:          16px;
-@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
-@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
-
-@font-size-h1:            floor((@font-size-base * 2)); // ~36px
-@font-size-h2:            floor((@font-size-base * 1.6)); // ~30px
-@font-size-h3:            ceil((@font-size-base * 1.25)); // ~24px
-@font-size-h4:            ceil((@font-size-base * 1.1)); // ~18px
-@font-size-h5:            @font-size-base;
-@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
-
-//** Unit-less `line-height` for use in components like buttons.
-@line-height-base:        1.5625; // 20/14
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
-
-//** By default, this inherits from the `<body>`.
-@headings-font-family:    @font-family-serif;
-@headings-font-weight:    500;
-@headings-line-height:    1.1;
-@headings-color:          @gray-dark;
-
-
-//-- Iconography
-//
-//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-@icon-font-path:          "../fonts/";
-@icon-font-name:          "glyphicons-halflings-regular";
-@icon-font-svg-id:        "glyphicons_halflingsregular";
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-@padding-base-vertical:     5px;
-@padding-base-horizontal:   16px;
-
-@padding-large-vertical:    10px;
-@padding-large-horizontal:  16px;
-
-@padding-small-vertical:    5px;
-@padding-small-horizontal:  10px;
-
-@padding-xs-vertical:       1px;
-@padding-xs-horizontal:     5px;
-
-@line-height-large:         1.33;
-@line-height-small:         1.5;
-
-@border-radius-base:        3px;
-@border-radius-large:       5px;
-@border-radius-small:       2px;
-
-//** Global color for active items (e.g., navs or dropdowns).
-@component-active-color:    #fff;
-//** Global background color for active items (e.g., navs or dropdowns).
-@component-active-bg:       @brand-primary;
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-@caret-width-base:          4px;
-//** Carets increase slightly in size for larger components.
-@caret-width-large:         5px;
-
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for `<th>`s and `<td>`s.
-@table-cell-padding:            8px;
-//** Padding for cells in `.table-condensed`.
-@table-condensed-cell-padding:  5px;
-
-//** Default background color used for all tables.
-@table-bg:                      transparent;
-//** Background color used for `.table-striped`.
-@table-bg-accent:               #f9f9f9;
-//** Background color used for `.table-hover`.
-@table-bg-hover:                #f5f5f5;
-@table-bg-active:               @table-bg-hover;
-
-//** Border color for table and cell borders.
-@table-border-color:            #ddd;
-
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-@btn-font-weight:                700;
-
-@btn-default-color:              #333;
-@btn-default-bg:                 #fff;
-@btn-default-border:             #ccc;
-
-@btn-primary-color:              #fff;
-@btn-primary-bg:                 @brand-primary;
-@btn-primary-border:             darken(@btn-primary-bg, 10%);
-
-@btn-success-color:              #fff;
-@btn-success-bg:                 @brand-success;
-@btn-success-border:             darken(@btn-success-bg, 10%);
-
-@btn-info-color:                 #fff;
-@btn-info-bg:                    @brand-info;
-@btn-info-border:                darken(@btn-info-bg, 15%);
-
-@btn-warning-color:              #fff;
-@btn-warning-bg:                 @brand-warning;
-@btn-warning-border:             darken(@btn-warning-bg, 10%);
-
-@btn-danger-color:               #fff;
-@btn-danger-bg:                  @brand-danger;
-@btn-danger-border:              darken(@btn-danger-bg, 10%);
-
-@btn-link-disabled-color:        @gray-light;
-
-
-//== Forms
-//
-//##
-
-//** `<input>` background color
-@input-bg:                       #fff;
-//** `<input disabled>` background color
-@input-bg-disabled:              @gray-lighter;
-
-//** Text color for `<input>`s
-@input-color:                    @gray;
-//** `<input>` border color
-@input-border:                   #ccc;
-//** `<input>` border radius
-@input-border-radius:            @border-radius-base;
-//** Border color for inputs on focus
-@input-border-focus:             #66afe9;
-
-//** Placeholder text color
-@input-color-placeholder:        @gray-light;
-
-//** Default `.form-control` height
-@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
-//** Large `.form-control` height
-@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
-//** Small `.form-control` height
-@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
-
-@legend-color:                   @gray-dark;
-@legend-border-color:            #e5e5e5;
-
-//** Background color for textual input addons
-@input-group-addon-bg:           @gray-lighter;
-//** Border color for textual input addons
-@input-group-addon-border-color: @input-border;
-
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-@dropdown-bg:                    #fff;
-//** Dropdown menu `border-color`.
-@dropdown-border:                rgba(0,0,0,.15);
-//** Dropdown menu `border-color` **for IE8**.
-@dropdown-fallback-border:       #ccc;
-//** Divider color for between dropdown items.
-@dropdown-divider-bg:            #e5e5e5;
-
-//** Dropdown link text color.
-@dropdown-link-color:            @gray-dark;
-//** Hover color for dropdown links.
-@dropdown-link-hover-color:      #fff;
-//** Hover background for dropdown links.
-@dropdown-link-hover-bg:         @brand-primary;
-
-//** Active dropdown menu item text color.
-@dropdown-link-active-color:     @component-active-color;
-//** Active dropdown menu item background color.
-@dropdown-link-active-bg:        @component-active-bg;
-
-//** Disabled dropdown menu item background color.
-@dropdown-link-disabled-color:   @gray-light;
-
-//** Text color for headers within dropdown menus.
-@dropdown-header-color:          @gray-light;
-
-// Note: Deprecated @dropdown-caret-color as of v3.1.0
-@dropdown-caret-color:           #000;
-
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-@zindex-navbar:            1000;
-@zindex-dropdown:          1000;
-@zindex-popover:           1010;
-@zindex-tooltip:           1030;
-@zindex-navbar-fixed:      1030;
-@zindex-modal-background:  1040;
-@zindex-modal:             1050;
-
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
-@screen-xs:                  480px;
-@screen-xs-min:              @screen-xs;
-@screen-phone:               @screen-xs-min;
-
-// Small screen / tablet
-// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1
-@screen-sm:                  768px;
-@screen-sm-min:              @screen-sm;
-@screen-tablet:              @screen-sm-min;
-
-// Medium screen / desktop
-// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1
-@screen-md:                  992px;
-@screen-md-min:              @screen-md;
-@screen-desktop:             @screen-md-min;
-
-// Large screen / wide desktop
-// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1
-@screen-lg:                  1200px;
-@screen-lg-min:              @screen-lg;
-@screen-lg-desktop:          @screen-lg-min;
-
-// So media queries don't overlap when required, provide a maximum
-@screen-xs-max:              (@screen-sm-min - 1);
-@screen-sm-max:              (@screen-md-min - 1);
-@screen-md-max:              (@screen-lg-min - 1);
-
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-@grid-columns:              12;
-//** Padding between columns. Gets divided in half for the left and right.
-@grid-gutter-width:         30px;
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-@grid-float-breakpoint:     @screen-sm-min;
-//** Point at which the navbar begins collapsing.
-@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
-
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-@container-tablet:             ((720px + @grid-gutter-width));
-//** For `@screen-sm-min` and up.
-@container-sm:                 @container-tablet;
-
-// Medium screen / desktop
-@container-desktop:            ((940px + @grid-gutter-width));
-//** For `@screen-md-min` and up.
-@container-md:                 @container-desktop;
-
-// Large screen / wide desktop
-@container-large-desktop:      ((1140px + @grid-gutter-width));
-//** For `@screen-lg-min` and up.
-@container-lg:                 @container-large-desktop;
-
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-@navbar-height:                    60px;
-@navbar-margin-bottom:             0;
-@navbar-border-radius:             0;
-@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
-@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
-@navbar-collapse-max-height:       340px;
-
-@navbar-default-color:             #777;
-@navbar-default-bg:                #fff;
-@navbar-default-border:            @gray-lighter;
-
-// Navbar links
-@navbar-default-link-color:                @link-color;
-@navbar-default-link-hover-color:          @link-hover-color;
-@navbar-default-link-hover-bg:             @link-hover-color;
-@navbar-default-link-active-color:         #fff;
-@navbar-default-link-active-bg:            @link-hover-color;
-@navbar-default-link-disabled-color:       #ccc;
-@navbar-default-link-disabled-bg:          transparent;
-
-// Navbar brand label
-@navbar-default-brand-color:               @navbar-default-link-color;
-@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
-@navbar-default-brand-hover-bg:            transparent;
-
-// Navbar toggle
-@navbar-default-toggle-hover-bg:           @link-hover-color;
-@navbar-default-toggle-border-color:       @link-color;
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-@nav-link-padding:                          10px 15px;
-@nav-link-hover-bg:                         @link-color;
-
-@nav-disabled-link-color:                   @gray-light;
-@nav-disabled-link-hover-color:             @gray-light;
-
-@nav-open-link-hover-color:                 #fff;
-
-//== Tabs
-@nav-tabs-border-color:                     #ddd;
-
-@nav-tabs-link-hover-border-color:          @link-color;
-
-@nav-tabs-active-link-hover-bg:             @body-bg;
-@nav-tabs-active-link-hover-color:          @gray;
-@nav-tabs-active-link-hover-border-color:   #ddd;
-
-@nav-tabs-justified-link-border-color:            #ddd;
-@nav-tabs-justified-active-link-border-color:     @body-bg;
-
-//== Pills
-@nav-pills-border-radius:                   @border-radius-base;
-@nav-pills-active-link-hover-bg:            @component-active-bg;
-@nav-pills-active-link-hover-color:         @component-active-color;
-
-
-//== Pagination
-//
-//##
-
-@pagination-color:                     @link-color;
-@pagination-bg:                        #fff;
-@pagination-border:                    #ddd;
-
-@pagination-hover-color:               @link-hover-color;
-@pagination-hover-bg:                  @gray-lighter;
-@pagination-hover-border:              #ddd;
-
-@pagination-active-color:              #fff;
-@pagination-active-bg:                 @brand-primary;
-@pagination-active-border:             @brand-primary;
-
-@pagination-disabled-color:            @gray-light;
-@pagination-disabled-bg:               #fff;
-@pagination-disabled-border:           #ddd;
-
-
-//== Pager
-//
-//##
-
-@pager-bg:                             @pagination-bg;
-@pager-border:                         @pagination-border;
-@pager-border-radius:                  15px;
-
-@pager-hover-bg:                       @pagination-hover-bg;
-
-@pager-active-bg:                      @pagination-active-bg;
-@pager-active-color:                   @pagination-active-color;
-
-@pager-disabled-color:                 @pagination-disabled-color;
-
-
-//== Jumbotron
-//
-//##
-
-@jumbotron-padding:              30px;
-@jumbotron-color:                inherit;
-@jumbotron-bg:                   @gray-lighter;
-@jumbotron-heading-color:        inherit;
-@jumbotron-font-size:            ceil((@font-size-base * 1.5));
-
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-@state-success-text:             darken(@brand-success, 20%);
-@state-success-bg:               lighten(@brand-success, 50%);
-@state-success-border:           darken(@brand-success, 5%);
-
-@state-info-text:                darken(@brand-info, 20%);
-@state-info-bg:                  lighten(@brand-info, 47%);
-@state-info-border:              darken(@brand-info, 7%);
-
-@state-warning-text:             darken(@brand-warning, 10%);
-@state-warning-bg:               lighten(@brand-warning, 45%);
-@state-warning-border:           @brand-warning;
-
-@state-danger-text:              darken(@brand-danger, 10%);
-@state-danger-bg:                lighten(@brand-danger, 50%);
-@state-danger-border:            darken(@brand-danger, 5%);
-
-
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-@tooltip-max-width:           200px;
-//** Tooltip text color
-@tooltip-color:               #fff;
-//** Tooltip background color
-@tooltip-bg:                  #000;
-@tooltip-opacity:             .9;
-
-//** Tooltip arrow width
-@tooltip-arrow-width:         5px;
-//** Tooltip arrow color
-@tooltip-arrow-color:         @tooltip-bg;
-
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-@popover-bg:                          #fff;
-//** Popover maximum width
-@popover-max-width:                   276px;
-//** Popover border color
-@popover-border-color:                rgba(0,0,0,.2);
-//** Popover fallback border color
-@popover-fallback-border-color:       #ccc;
-
-//** Popover title background color
-@popover-title-bg:                    darken(@popover-bg, 3%);
-
-//** Popover arrow width
-@popover-arrow-width:                 10px;
-//** Popover arrow color
-@popover-arrow-color:                 #fff;
-
-//** Popover outer arrow width
-@popover-arrow-outer-width:           (@popover-arrow-width + 1);
-//** Popover outer arrow color
-@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
-//** Popover outer arrow fallback color
-@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
-
-
-//== Labels
-//
-//##
-
-//** Default label background color
-@label-default-bg:            @gray-light;
-//** Primary label background color
-@label-primary-bg:            @brand-primary;
-//** Success label background color
-@label-success-bg:            @brand-success;
-//** Info label background color
-@label-info-bg:               @brand-info;
-//** Warning label background color
-@label-warning-bg:            @brand-warning;
-//** Danger label background color
-@label-danger-bg:             @brand-danger;
-
-//** Default label text color
-@label-color:                 #fff;
-//** Default text color of a linked label
-@label-link-hover-color:      #fff;
-
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-@modal-inner-padding:         20px;
-
-//** Padding applied to the modal title
-@modal-title-padding:         15px;
-//** Modal title line-height
-@modal-title-line-height:     @line-height-base;
-
-//** Background color of modal content area
-@modal-content-bg:                             #fff;
-//** Modal content border color
-@modal-content-border-color:                   rgba(0,0,0,.2);
-//** Modal content border color **for IE8**
-@modal-content-fallback-border-color:          #999;
-
-//** Modal backdrop background color
-@modal-backdrop-bg:           #000;
-//** Modal backdrop opacity
-@modal-backdrop-opacity:      .5;
-//** Modal header border color
-@modal-header-border-color:   #e5e5e5;
-//** Modal footer border color
-@modal-footer-border-color:   @modal-header-border-color;
-@modal-footer-background-color:   @gray-lightest;
-
-@modal-lg:                    900px;
-@modal-md:                    600px;
-@modal-sm:                    300px;
-
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-@alert-padding:               15px;
-@alert-border-radius:         @border-radius-base;
-@alert-link-font-weight:      bold;
-
-@alert-success-bg:            @state-success-bg;
-@alert-success-text:          @state-success-text;
-@alert-success-border:        @state-success-border;
-
-@alert-info-bg:               @state-info-bg;
-@alert-info-text:             @state-info-text;
-@alert-info-border:           @state-info-border;
-
-@alert-warning-bg:            @state-warning-bg;
-@alert-warning-text:          @state-warning-text;
-@alert-warning-border:        @state-warning-border;
-
-@alert-danger-bg:             @state-danger-bg;
-@alert-danger-text:           @state-danger-text;
-@alert-danger-border:         @state-danger-border;
-
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-@progress-bg:                 white;
-@progress-border-color:       @gray-lighter;
-//** Progress bar text color
-@progress-bar-color:          #fff;
-
-//** Default progress bar color
-@progress-bar-bg:             @brand-primary;
-//** Success progress bar color
-@progress-bar-success-bg:     @brand-success;
-//** Warning progress bar color
-@progress-bar-warning-bg:     @brand-warning;
-//** Danger progress bar color
-@progress-bar-danger-bg:      @brand-danger;
-//** Info progress bar color
-@progress-bar-info-bg:        @brand-info;
-
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-@list-group-bg:                 #fff;
-//** `.list-group-item` border color
-@list-group-border:             #ddd;
-//** List group border radius
-@list-group-border-radius:      @border-radius-base;
-
-//** Background color of single list elements on hover
-@list-group-hover-bg:           #f5f5f5;
-//** Text color of active list elements
-@list-group-active-color:       @component-active-color;
-//** Background color of active list elements
-@list-group-active-bg:          @component-active-bg;
-//** Border color of active list elements
-@list-group-active-border:      @list-group-active-bg;
-@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
-
-@list-group-link-color:         #555;
-@list-group-link-heading-color: #333;
-
-
-//== Panels
-//
-//##
-
-@panel-bg:                    #fff;
-@panel-body-padding:          15px;
-@panel-border-radius:         @border-radius-base;
-
-//** Border color for elements within panels
-@panel-inner-border:          #ddd;
-@panel-footer-bg:             #f5f5f5;
-
-@panel-default-text:          @gray-dark;
-@panel-default-border:        #ddd;
-@panel-default-heading-bg:    #f5f5f5;
-
-@panel-primary-text:          #fff;
-@panel-primary-border:        @brand-primary;
-@panel-primary-heading-bg:    @brand-primary;
-
-@panel-success-text:          @state-success-text;
-@panel-success-border:        @state-success-border;
-@panel-success-heading-bg:    @state-success-bg;
-
-@panel-info-text:             @state-info-text;
-@panel-info-border:           @state-info-border;
-@panel-info-heading-bg:       @state-info-bg;
-
-@panel-warning-text:          @state-warning-text;
-@panel-warning-border:        @state-warning-border;
-@panel-warning-heading-bg:    @state-warning-bg;
-
-@panel-danger-text:           @state-danger-text;
-@panel-danger-border:         @state-danger-border;
-@panel-danger-heading-bg:     @state-danger-bg;
-
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-@thumbnail-padding:           4px;
-//** Thumbnail background color
-@thumbnail-bg:                @body-bg;
-//** Thumbnail border color
-@thumbnail-border:            #ddd;
-//** Thumbnail border radius
-@thumbnail-border-radius:     @border-radius-base;
-
-//** Custom text color for thumbnail captions
-@thumbnail-caption-color:     @text-color;
-//** Padding around the thumbnail caption
-@thumbnail-caption-padding:   9px;
-
-
-//== Wells
-//
-//##
-
-@well-bg:                     #f5f5f5;
-@well-border:                 darken(@well-bg, 7%);
-
-
-//== Badges
-//
-//##
-
-@badge-color:                 #fff;
-//** Linked badge text color on hover
-@badge-link-hover-color:      #fff;
-@badge-bg:                    @gray-light;
-
-//** Badge text color in active nav link
-@badge-active-color:          @link-color;
-//** Badge background color in active nav link
-@badge-active-bg:             #fff;
-
-@badge-font-weight:           bold;
-@badge-line-height:           1;
-@badge-border-radius:         10px;
-
-
-//== Breadcrumbs
-//
-//##
-
-@breadcrumb-padding-vertical:   8px;
-@breadcrumb-padding-horizontal: 15px;
-//** Breadcrumb background color
-@breadcrumb-bg:                 #f5f5f5;
-//** Breadcrumb text color
-@breadcrumb-color:              #ccc;
-//** Text color of current page in the breadcrumb
-@breadcrumb-active-color:       @gray-light;
-//** Textual separator for between breadcrumb elements
-@breadcrumb-separator:          "/";
-
-
-//== Carousel
-//
-//##
-
-@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
-
-@carousel-control-color:                      #fff;
-@carousel-control-width:                      15%;
-@carousel-control-opacity:                    .5;
-@carousel-control-font-size:                  20px;
-
-@carousel-indicator-active-bg:                #fff;
-@carousel-indicator-border-color:             #fff;
-
-@carousel-caption-color:                      #fff;
-
-
-//== Close
-//
-//##
-
-@close-font-weight:           bold;
-@close-color:                 #000;
-@close-text-shadow:           0 1px 0 #fff;
-
-
-//== Code
-//
-//##
-
-@code-color:                  #c7254e;
-@code-bg:                     #f9f2f4;
-
-@kbd-color:                   #fff;
-@kbd-bg:                      #333;
-
-@pre-bg:                      #f5f5f5;
-@pre-color:                   @gray-dark;
-@pre-border-color:            #ccc;
-@pre-scrollable-max-height:   340px;
-
-
-//== Type
-//
-//##
-
-//** Text muted color
-@text-muted:                  @gray-light;
-//** Abbreviations and acronyms border color
-@abbr-border-color:           @gray-light;
-//** Headings small color
-@headings-small-color:        @gray-light;
-//** Blockquote small color
-@blockquote-small-color:      @gray;
-//** Blockquote font size
-@blockquote-font-size:        (@font-size-base * 1.125);
-//** Blockquote border color
-@blockquote-border-color:     @gray-lighter;
-//** Page header border color
-@page-header-border-color:    @gray-lighter;
-
-
-//== Miscellaneous
-//
-//##
-
-//** Horizontal line color.
-@hr-border:                   @gray-lighter;
-
-//** Horizontal offset for forms and lists.
-@component-offset-horizontal: 180px;
-
-@content-margin-top: @line-height-computed;
-@content-margin-top: @line-height-computed;
-
-// Custom
-
-@left-menu-width: 260px;
-@left-menu-animation-duration: 0.35s;
+@navbar-brand-image-url: url(/img/brand/logo-horizontal.svg);
 
-@toolbar-border-color: @gray-lighter;
-@file-tree-droppable-background-color: rgb(252, 231, 199);
+@editor-loading-logo-padding-top: 86.2%;
+@editor-loading-logo-background-url: url(/img/brand/lion-grey.svg);
+@editor-loading-logo-foreground-url: url(/img/brand/lion.svg);
 
-@editor-dark-background-color: #333;
-@editor-dark-toolbar-border-color: #222;
-@editor-dark-highlight-color: #FFA03A;
+@import "_common-variables.less";

+ 3 - 0
services/web/public/stylesheets/ol-style.less

@@ -0,0 +1,3 @@
+// Core variables and mixins
+@import "core/ol-variables.less";
+@import "_style_includes.less";

+ 1 - 84
services/web/public/stylesheets/style.less

@@ -1,86 +1,3 @@
 // Core variables and mixins
 @import "core/variables.less";
-@import url(https://netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css);
-
-@import "core/mixins.less";
-
-// Reset
-@import "core/normalize.less";
-@import "core/print.less";
-
-// Core CSS
-@import "core/scaffolding.less";
-@import "core/type.less";
-@import "core/grid.less";
-
-// Components
-@import "components/tables.less";
-@import "components/forms.less";
-@import "components/buttons.less";
-@import "components/card.less";
-//@import "components/code.less";
-@import "components/component-animations.less";
-@import "components/glyphicons.less";
-@import "components/dropdowns.less";
-@import "components/button-groups.less";
-@import "components/input-groups.less";
-@import "components/navs.less";
-@import "components/navbar.less";
-@import "components/footer.less";
-//@import "components/breadcrumbs.less";
-//@import "components/pagination.less";
-@import "components/pager.less";
-@import "components/labels.less";
-//@import "components/badges.less";
-//@import "components/jumbotron.less";
-@import "components/thumbnails.less";
-@import "components/alerts.less";
-@import "components/progress-bars.less";
-// @import "components/media.less";
-// @import "components/list-group.less";
-// @import "components/panels.less";
-// @import "components/wells.less";
-@import "components/close.less";
-@import "components/fineupload.less";
-@import "components/hover.less";
-
-// Components w/ JavaScript
-@import "components/modals.less";
-@import "components/tooltip.less";
-@import "components/popovers.less";
-@import "components/carousel.less";
-
-// ngTagsInput
-@import "components/tags-input.less";
-
-// Utility classes
-@import "core/utilities.less";
-@import "core/responsive-utilities.less";
-
-// ShareLaTeX app classes
-@import "app/base.less";
-@import "app/account-settings.less";
-@import "app/beta-program.less";
-@import "app/about-page.less";
-@import "app/project-list.less";
-@import "app/editor.less";
-@import "app/homepage.less";
-@import "app/plans.less";
-@import "app/recurly.less";
-@import "app/bonus.less";
-@import "app/register.less";
-@import "app/blog.less";
-@import "app/features.less";
-@import "app/templates.less";
-@import "app/wiki.less";
-@import "app/translations.less";
-@import "app/contact-us.less";
-@import "app/subscription.less";
-@import "app/sprites.less";
-@import "app/invite.less";
-@import "app/review-features-page.less";
-@import "app/error-pages.less";
-
-@import	"../js/libs/pdfListView/TextLayer.css";
-@import	"../js/libs/pdfListView/AnnotationsLayer.css";
-@import	"../js/libs/pdfListView/HighlightsLayer.css";
+@import "_style_includes.less";

+ 18 - 0
services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee

@@ -116,6 +116,24 @@ describe "AuthenticationManager", ->
 					expect(err).to.exist
 					done()
 
+			it "should not start the bcrypt process", (done)->
+				@AuthenticationManager.setUserPassword @user_id, @password, (err)=>
+					@bcrypt.genSalt.called.should.equal false
+					@bcrypt.hash.called.should.equal false
+					done()
+
+		describe "too short", ->
+			beforeEach ->
+				@settings.passwordStrengthOptions =
+					length:
+						max:10
+						min:6
+				@password = "dsd"
+
+			it "should return and error", (done)->
+				@AuthenticationManager.setUserPassword @user_id, @password, (err)->
+					expect(err).to.exist
+					done()
 
 			it "should not start the bcrypt process", (done)->
 				@AuthenticationManager.setUserPassword @user_id, @password, (err)=>

+ 33 - 0
services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee

@@ -37,6 +37,7 @@ describe "SubscriptionUpdater", ->
 			constructor: (opts)-> 
 				subscription.admin_id = opts.admin_id
 				return subscription
+			@remove: sinon.stub().yields()
 		@SubscriptionModel.update = @updateStub
 		@SubscriptionModel.findAndModify = @findAndModifyStub
 
@@ -230,3 +231,35 @@ describe "SubscriptionUpdater", ->
 				@ReferalAllocator.assignBonus.calledWith(@adminuser_id).should.equal true
 				done()
 
+	describe "deleteSubscription", ->
+		beforeEach (done) ->
+			@subscription_id = ObjectId().toString()
+			@subscription = {
+				"mock": "subscription",
+				admin_id: ObjectId(),
+				member_ids: [ ObjectId(), ObjectId(), ObjectId() ]
+			}
+			@SubscriptionLocator.getSubscription = sinon.stub().yields(null, @subscription)
+			@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().yields()
+			@SubscriptionUpdater.deleteSubscription @subscription_id, done
+			
+		it "should look up the subscription", ->
+			@SubscriptionLocator.getSubscription
+				.calledWith(@subscription_id)
+				.should.equal true
+		
+		it "should remove the subscription", ->
+			@SubscriptionModel.remove
+				.calledWith({_id: ObjectId(@subscription_id)})
+				.should.equal true
+		
+		it "should downgrade the admin_id", ->
+			@SubscriptionUpdater._setUsersMinimumFeatures
+				.calledWith(@subscription.admin_id)
+				.should.equal true
+		
+		it "should downgrade all of the members", ->
+			for user_id in @subscription.member_ids
+				@SubscriptionUpdater._setUsersMinimumFeatures
+					.calledWith(user_id)
+					.should.equal true

+ 1 - 1
services/web/test/smoke/coffee/SmokeTests.coffee

@@ -94,7 +94,7 @@ describe "Opening", ->
 		
 			expect(error, "smoke test: error returned in getting project list").to.not.exist
 			expect(!!stderr.match("200 OK"), "smoke test: response code is not 200 getting project list").to.equal true
-			expect(!!stdout.match("<title>Your Projects - ShareLaTeX, Online LaTeX Editor</title>"), "smoke test: body does not have correct title").to.equal true
+			expect(!!stdout.match("<title>Your Projects - .*, Online LaTeX Editor</title>"), "smoke test: body does not have correct title").to.equal true
 			expect(!!stdout.match("ProjectPageController"), "smoke test: body does not have correct angular controller").to.equal true
 			done()