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

Merge branch 'master' into sk-launchpad

Shane Kilkelly 9 лет назад
Родитель
Сommit
06bba062a4
19 измененных файлов с 1389 добавлено и 275 удалено
  1. 1 0
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  2. 2 1
      services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
  3. 3 0
      services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
  4. 1 1
      services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
  5. 49 0
      services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee
  6. 44 58
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  7. 32 17
      services/web/app/coffee/Features/Email/EmailSender.coffee
  8. 380 0
      services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee
  9. 2 11
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  10. 35 7
      services/web/app/views/layout/navbar.jade
  11. 2 0
      services/web/app/views/project/editor/share.jade
  12. 5 29
      services/web/config/settings.defaults.coffee
  13. 10 4
      services/web/public/js/ace-1.2.5/mode-latex.js
  14. 565 112
      services/web/public/js/ace-1.2.5/worker-latex.js
  15. 188 33
      services/web/public/js/ace-1.2.5/worker-latex_beta.js
  16. 7 0
      services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
  17. 31 0
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
  18. 2 2
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
  19. 30 0
      services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee

+ 1 - 0
services/web/app/coffee/Features/Authentication/AuthenticationController.coffee

@@ -148,6 +148,7 @@ module.exports = AuthenticationController =
 			return next()
 			return next()
 		else
 		else
 			logger.log url:req.url, "user trying to access endpoint not in global whitelist"
 			logger.log url:req.url, "user trying to access endpoint not in global whitelist"
+			AuthenticationController._setRedirectInSession(req)
 			return res.redirect "/login"
 			return res.redirect "/login"
 
 
 	httpAuth: basicAuth (user, pass)->
 	httpAuth: basicAuth (user, pass)->

+ 2 - 1
services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee

@@ -11,7 +11,7 @@ module.exports = CollaboratorsEmailHandler =
 			"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
 			"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
 		].join("&")
 		].join("&")
 
 
-	notifyUserOfProjectInvite: (project_id, email, invite, callback)->
+	notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
 		Project
 		Project
 			.findOne(_id: project_id )
 			.findOne(_id: project_id )
 			.select("name owner_ref")
 			.select("name owner_ref")
@@ -24,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
 						name: project.name
 						name: project.name
 					inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
 					inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
 					owner: project.owner_ref
 					owner: project.owner_ref
+					sendingUser_id: sendingUser._id
 				EmailHandler.sendEmail "projectInvite", emailOptions, callback
 				EmailHandler.sendEmail "projectInvite", emailOptions, callback

+ 3 - 0
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee

@@ -37,6 +37,9 @@ module.exports = CollaboratorsInviteController =
 		email = req.body.email
 		email = req.body.email
 		sendingUser = AuthenticationController.getSessionUser(req)
 		sendingUser = AuthenticationController.getSessionUser(req)
 		sendingUserId = sendingUser._id
 		sendingUserId = sendingUser._id
+		if email == sendingUser.email
+			logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project"
+			return res.json {invite: null, error: 'cannot_invite_self'}
 		logger.log {projectId, email, sendingUserId}, "inviting to project"
 		logger.log {projectId, email, sendingUserId}, "inviting to project"
 		LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
 		LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
 			return next(error) if error?
 			return next(error) if error?

+ 1 - 1
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee

@@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
 
 
 	_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
 	_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
 		logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
 		logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
-		CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)->
+		CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)->
 			return callback(err) if err?
 			return callback(err) if err?
 			CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
 			CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
 				return callback(err) if err?
 				return callback(err) if err?

+ 49 - 0
services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee

@@ -0,0 +1,49 @@
+_ = require("underscore")
+settings = require "settings-sharelatex"
+
+module.exports = _.template """
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
+	<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
+		<h3 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: px; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: px; padding: 0; text-align: left; word-wrap: normal;">
+			<%= title %>
+		</h3>
+		<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+		<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
+			<%= greeting %>
+		</p>
+		<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
+			<%= message %>
+		</p>
+		<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+		<center data-parsed="" style="min-width: 532px; width: 100%;">
+			<table class="button float-center" style="Margin: 0 0 16px 0; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 0 16px 0; padding: 0; text-align: center; vertical-align: top; width: auto;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #a93529; border: 2px solid #a93529; border-collapse: collapse !important; color: #fefefe; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+				<a href="<%= ctaURL %>" style="Margin: 0; border: 0 solid #a93529; border-radius: 3px; color: #fefefe; display: inline-block; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; line-height: 1.3; margin: 0; padding: 8px 16px 8px 16px; text-align: left; text-decoration: none;">
+					<%= ctaText %>
+				</a>
+			</td></tr></table></td></tr></table>
+		</center>
+		<% if (secondaryMessage) { %>
+			<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+			<p class="avoid-auto-linking" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
+				<%= secondaryMessage %>
+			</p>
+		<% } %>
+	</th>
+	<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
+</tr></tbody></table>
+<% if (gmailGoToAction) { %>
+	<script type="application/ld+json">
+		{
+			"@context": "http://schema.org",
+			"@type": "EmailMessage",
+			"potentialAction": {
+				"@type": "ViewAction",
+				"target": "<%= gmailGoToAction.target %>",
+				"url": "<%= gmailGoToAction.target %>",
+				"name": "<%= gmailGoToAction.name %>"
+			},
+			"description": "<%= gmailGoToAction.description %>"
+		}
+	</script>
+<% } %>
+"""

+ 44 - 58
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -1,6 +1,12 @@
 _ = require('underscore')
 _ = require('underscore')
+
 PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
 PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
 NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
 NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
+BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout") 
+
+SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
+
+
 settings = require("settings-sharelatex")
 settings = require("settings-sharelatex")
 
 
 
 
@@ -61,7 +67,7 @@ ShareLaTeX Co-founder
 
 
 templates.passwordResetRequested =
 templates.passwordResetRequested =
 	subject:  _.template "Password Reset - #{settings.appName}"
 	subject:  _.template "Password Reset - #{settings.appName}"
-	layout: NotificationEmailLayout
+	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	type:"notification"
 	plainTextTemplate: _.template """
 	plainTextTemplate: _.template """
 Password Reset
 Password Reset
@@ -78,36 +84,21 @@ Thank you
 
 
 #{settings.appName} - <%= siteUrl %>
 #{settings.appName} - <%= siteUrl %>
 """
 """
-	compiledTemplate: _.template """
-<h2>Password Reset</h2>
-<p>
-We got a request to reset your #{settings.appName} password.
-<p>
-<center>
-	<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
-		<div style="padding-right:10px;padding-left:10px">
-			<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
-		  			Reset password
-				</span>
-			</a>
-		</div>
-	</div>
-</center>
-
-If you ignore this message, your password won't be changed.
-<p>
-If you didn't request a password reset, let us know.
-
-</p>
-<p>Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
+	compiledTemplate: (opts) -> 
+		SingleCTAEmailBody({
+			title: "Password Reset"
+			greeting: "Hi,"
+			message: "We got a request to reset your #{settings.appName} password."
+			secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
+			ctaText: "Reset password"
+			ctaURL: opts.setNewPasswordUrl
+			gmailGoToAction: null
+		})
 
 
 
 
 templates.projectInvite =
 templates.projectInvite =
 	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
 	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
-	layout: NotificationEmailLayout
+	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	type:"notification"
 	plainTextTemplate: _.template """
 	plainTextTemplate: _.template """
 Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
 Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
@@ -118,23 +109,25 @@ Thank you
 
 
 #{settings.appName} - <%= siteUrl %>
 #{settings.appName} - <%= siteUrl %>
 """
 """
-	compiledTemplate: _.template """
-<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
-<center>
-			<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
-					View Project
-				</span>
-			</a>
-</center>
-<p> Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
+	compiledTemplate: (opts) -> 
+		SingleCTAEmailBody({
+			title: "#{ opts.project.name } &ndash; shared by #{ opts.owner.email }"
+			greeting: "Hi,"
+			message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name }&rdquo; with you."
+			secondaryMessage: null
+			ctaText: "View project"
+			ctaURL: opts.inviteUrl
+			gmailGoToAction: 
+				target: opts.inviteUrl
+				name: "View project"
+				description: "Join #{ opts.project.name } at ShareLaTeX"
+		})
+
 
 
 
 
 templates.completeJoinGroupAccount =
 templates.completeJoinGroupAccount =
 	subject: _.template "Verify Email to join <%= group_name %> group"
 	subject: _.template "Verify Email to join <%= group_name %> group"
-	layout: NotificationEmailLayout
+	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	type:"notification"
 	plainTextTemplate: _.template """
 	plainTextTemplate: _.template """
 Hi, please verify your email to join the <%= group_name %> and get your free premium account
 Hi, please verify your email to join the <%= group_name %> and get your free premium account
@@ -145,23 +138,16 @@ Thank You
 
 
 #{settings.appName} - <%= siteUrl %>
 #{settings.appName} - <%= siteUrl %>
 """
 """
-	compiledTemplate: _.template """
-<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
-<center>
-	<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
-		<div style="padding-right:10px;padding-left:10px">
-			<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
-		  			Verify now
-				</span>
-			</a>
-		</div>
-	</div>
-</center>
-<p> Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
-
+	compiledTemplate: (opts) -> 
+		SingleCTAEmailBody({
+			title: "Verify Email to join #{ opts.group_name } group"
+			greeting: "Hi,"
+			message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
+			secondaryMessage: null
+			ctaText: "Verify now"
+			ctaURL: opts.completeJoinUrl
+			gmailGoToAction: null
+		})
 
 
 module.exports =
 module.exports =
 	templates: templates
 	templates: templates
@@ -177,4 +163,4 @@ module.exports =
 			html: template.layout(opts)
 			html: template.layout(opts)
 			text: template?.plainTextTemplate?(opts)
 			text: template?.plainTextTemplate?(opts)
 			type:template.type
 			type:template.type
-		}
+		}

+ 32 - 17
services/web/app/coffee/Features/Email/EmailSender.coffee

@@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
 nodemailer = require("nodemailer")
 nodemailer = require("nodemailer")
 sesTransport = require('nodemailer-ses-transport')
 sesTransport = require('nodemailer-ses-transport')
 sgTransport = require('nodemailer-sendgrid-transport')
 sgTransport = require('nodemailer-sendgrid-transport')
-
+rateLimiter = require('../../infrastructure/RateLimiter')
 _ = require("underscore")
 _ = require("underscore")
 
 
 if Settings.email? and Settings.email.fromAddress?
 if Settings.email? and Settings.email.fromAddress?
@@ -39,24 +39,39 @@ if nm_client?
 else
 else
 	logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
 	logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
 
 
+checkCanSendEmail = (options, callback)->
+	if !options.sendingUser_id? #email not sent from user, not rate limited
+		callback(null, true)
+	opts = 
+		endpointName: "send_email"
+		timeInterval: 60 * 60 * 3
+		subjectName: options.sendingUser_id
+		throttle: 100
+	rateLimiter.addCount opts, callback
 
 
 module.exports =
 module.exports =
 	sendEmail : (options, callback = (error) ->)->
 	sendEmail : (options, callback = (error) ->)->
 		logger.log receiver:options.to, subject:options.subject, "sending email"
 		logger.log receiver:options.to, subject:options.subject, "sending email"
-		metrics.inc "email"
-		options =
-			to: options.to
-			from: defaultFromAddress
-			subject: options.subject
-			html: options.html
-			text: options.text
-			replyTo: options.replyTo || Settings.email.replyToAddress
-			socketTimeout: 30 * 1000
-		if Settings.email.textEncoding?
-			opts.textEncoding = textEncoding
-		client.sendMail options, (err, res)->
+		checkCanSendEmail options, (err, canContinue)->
 			if err?
 			if err?
-				logger.err err:err, "error sending message"
-			else
-				logger.log "Message sent to #{options.to}"
-			callback(err)
+				return callback(err)
+			if !canContinue
+				logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
+				return callback("rate limit hit sending email")
+			metrics.inc "email"
+			options =
+				to: options.to
+				from: defaultFromAddress
+				subject: options.subject
+				html: options.html
+				text: options.text
+				replyTo: options.replyTo || Settings.email.replyToAddress
+				socketTimeout: 30 * 1000
+			if Settings.email.textEncoding?
+				opts.textEncoding = textEncoding
+			client.sendMail options, (err, res)->
+				if err?
+					logger.err err:err, "error sending message"
+				else
+					logger.log "Message sent to #{options.to}"
+				callback(err)

+ 380 - 0
services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee

@@ -0,0 +1,380 @@
+_ = require("underscore")
+settings = require "settings-sharelatex"
+
+module.exports = _.template """
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="Margin: 0; background: #f6f6f6 !important; margin: 0; min-height: 100%; padding: 0;">
+	<head>
+		
+		
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+		<meta name="viewport" content="width=device-width">
+		<title>Project invite</title>
+		<style>.avoid-auto-linking a,
+.avoid-auto-linking a[href] {
+	color: #a93529 !important;
+	text-decoration: none !important;
+	-moz-hyphens: none;
+	-ms-hyphens: none;
+	-webkit-hyphens: none;
+	hyphens: none; }
+	.avoid-auto-linking a:visited,
+	.avoid-auto-linking a[href]:visited {
+		color: #a93529; }
+	.avoid-auto-linking a:hover,
+	.avoid-auto-linking a[href]:hover {
+		color: #80281f; }
+	.avoid-auto-linking a:active,
+	.avoid-auto-linking a[href]:active {
+		color: #80281f; }
+@media only screen {
+	html {
+		min-height: 100%;
+		background: #f6f6f6;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	.small-float-center {
+		margin: 0 auto !important;
+		float: none !important;
+		text-align: center !important;
+	}
+
+	.small-text-center {
+		text-align: center !important;
+	}
+
+	.small-text-left {
+		text-align: left !important;
+	}
+
+	.small-text-right {
+		text-align: right !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	.hide-for-large {
+		display: block !important;
+		width: auto !important;
+		overflow: visible !important;
+		max-height: none !important;
+		font-size: inherit !important;
+		line-height: inherit !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body table.container .hide-for-large,
+	table.body table.container .row.hide-for-large {
+		display: table !important;
+		width: 100% !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body table.container .callout-inner.hide-for-large {
+		display: table-cell !important;
+		width: 100% !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body table.container .show-for-large {
+		display: none !important;
+		width: 0;
+		mso-hide: all;
+		overflow: hidden;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body img {
+		width: auto;
+		height: auto;
+	}
+
+	table.body center {
+		min-width: 0 !important;
+	}
+
+	table.body .container {
+		width: 95% !important;
+	}
+
+	table.body .columns,
+	table.body .column {
+		height: auto !important;
+		-moz-box-sizing: border-box;
+		-webkit-box-sizing: border-box;
+		box-sizing: border-box;
+		padding-left: 16px !important;
+		padding-right: 16px !important;
+	}
+
+	table.body .columns .column,
+	table.body .columns .columns,
+	table.body .column .column,
+	table.body .column .columns {
+		padding-left: 0 !important;
+		padding-right: 0 !important;
+	}
+
+	table.body .collapse .columns,
+	table.body .collapse .column {
+		padding-left: 0 !important;
+		padding-right: 0 !important;
+	}
+
+	td.small-1,
+	th.small-1 {
+		display: inline-block !important;
+		width: 8.33333% !important;
+	}
+
+	td.small-2,
+	th.small-2 {
+		display: inline-block !important;
+		width: 16.66667% !important;
+	}
+
+	td.small-3,
+	th.small-3 {
+		display: inline-block !important;
+		width: 25% !important;
+	}
+
+	td.small-4,
+	th.small-4 {
+		display: inline-block !important;
+		width: 33.33333% !important;
+	}
+
+	td.small-5,
+	th.small-5 {
+		display: inline-block !important;
+		width: 41.66667% !important;
+	}
+
+	td.small-6,
+	th.small-6 {
+		display: inline-block !important;
+		width: 50% !important;
+	}
+
+	td.small-7,
+	th.small-7 {
+		display: inline-block !important;
+		width: 58.33333% !important;
+	}
+
+	td.small-8,
+	th.small-8 {
+		display: inline-block !important;
+		width: 66.66667% !important;
+	}
+
+	td.small-9,
+	th.small-9 {
+		display: inline-block !important;
+		width: 75% !important;
+	}
+
+	td.small-10,
+	th.small-10 {
+		display: inline-block !important;
+		width: 83.33333% !important;
+	}
+
+	td.small-11,
+	th.small-11 {
+		display: inline-block !important;
+		width: 91.66667% !important;
+	}
+
+	td.small-12,
+	th.small-12 {
+		display: inline-block !important;
+		width: 100% !important;
+	}
+
+	.columns td.small-12,
+	.column td.small-12,
+	.columns th.small-12,
+	.column th.small-12 {
+		display: block !important;
+		width: 100% !important;
+	}
+
+	table.body td.small-offset-1,
+	table.body th.small-offset-1 {
+		margin-left: 8.33333% !important;
+		Margin-left: 8.33333% !important;
+	}
+
+	table.body td.small-offset-2,
+	table.body th.small-offset-2 {
+		margin-left: 16.66667% !important;
+		Margin-left: 16.66667% !important;
+	}
+
+	table.body td.small-offset-3,
+	table.body th.small-offset-3 {
+		margin-left: 25% !important;
+		Margin-left: 25% !important;
+	}
+
+	table.body td.small-offset-4,
+	table.body th.small-offset-4 {
+		margin-left: 33.33333% !important;
+		Margin-left: 33.33333% !important;
+	}
+
+	table.body td.small-offset-5,
+	table.body th.small-offset-5 {
+		margin-left: 41.66667% !important;
+		Margin-left: 41.66667% !important;
+	}
+
+	table.body td.small-offset-6,
+	table.body th.small-offset-6 {
+		margin-left: 50% !important;
+		Margin-left: 50% !important;
+	}
+
+	table.body td.small-offset-7,
+	table.body th.small-offset-7 {
+		margin-left: 58.33333% !important;
+		Margin-left: 58.33333% !important;
+	}
+
+	table.body td.small-offset-8,
+	table.body th.small-offset-8 {
+		margin-left: 66.66667% !important;
+		Margin-left: 66.66667% !important;
+	}
+
+	table.body td.small-offset-9,
+	table.body th.small-offset-9 {
+		margin-left: 75% !important;
+		Margin-left: 75% !important;
+	}
+
+	table.body td.small-offset-10,
+	table.body th.small-offset-10 {
+		margin-left: 83.33333% !important;
+		Margin-left: 83.33333% !important;
+	}
+
+	table.body td.small-offset-11,
+	table.body th.small-offset-11 {
+		margin-left: 91.66667% !important;
+		Margin-left: 91.66667% !important;
+	}
+
+	table.body table.columns td.expander,
+	table.body table.columns th.expander {
+		display: none !important;
+	}
+
+	table.body .right-text-pad,
+	table.body .text-pad-right {
+		padding-left: 10px !important;
+	}
+
+	table.body .left-text-pad,
+	table.body .text-pad-left {
+		padding-right: 10px !important;
+	}
+
+	table.menu {
+		width: 100% !important;
+	}
+
+	table.menu td,
+	table.menu th {
+		width: auto !important;
+		display: inline-block !important;
+	}
+
+	table.menu.vertical td,
+	table.menu.vertical th,
+	table.menu.small-vertical td,
+	table.menu.small-vertical th {
+		display: block !important;
+	}
+
+	table.menu[align="center"] {
+		width: auto !important;
+	}
+
+	table.button.small-expand,
+	table.button.small-expanded {
+		width: 100% !important;
+	}
+
+	table.button.small-expand table,
+	table.button.small-expanded table {
+		width: 100%;
+	}
+
+	table.button.small-expand table a,
+	table.button.small-expanded table a {
+		text-align: center !important;
+		width: 100% !important;
+		padding-left: 0 !important;
+		padding-right: 0 !important;
+	}
+
+	table.button.small-expand center,
+	table.button.small-expanded center {
+		min-width: 0;
+	}
+}</style>
+	</head>
+	<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#F6F6F6" style="-moz-box-sizing: border-box; -ms-text-size-adjust: 100%; -webkit-box-sizing: border-box; -webkit-text-size-adjust: 100%; Margin: 0; background: #f6f6f6 !important; box-sizing: border-box; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; min-height: 100%; min-width: 100%; padding: 0; text-align: left; width: 100% !important;">
+		<!-- <span class="preheader"></span> -->
+		<table class="body" border="0" cellspacing="0" cellpadding="0" width="100%" height="100%" style="Margin: 0; background: #f6f6f6 !important; border-collapse: collapse; border-spacing: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; height: 100%; line-height: 1.3; margin: 0; min-height: 100%; padding: 0; text-align: left; vertical-align: top; width: 100%;">
+			<tr style="padding: 0; text-align: left; vertical-align: top;">
+				<td class="body-cell" align="center" valign="top" bgcolor="#F6F6F6" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #f6f6f6 !important; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; padding-bottom: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
+					<center data-parsed="" style="min-width: 580px; width: 100%;">
+						
+						<table align="center" class="wrapper header float-center" style="Margin: 0 auto; background: #fefefe; border-bottom: solid 1px #cfcfcf; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
+							<table align="center" class="container" style="Margin: 0 auto; background: transparent; border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+							<table class="row collapse" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
+								<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 0; padding-left: 0; padding-right: 0; text-align: left; width: 588px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
+								<h1 class="sl-logotype" style="Margin: 0; Margin-bottom: 0; color: #333333; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 26px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 0; padding: 0; text-align: left; word-wrap: normal;">
+									<span>S</span><span class="sl-logotype-small" style="font-size: 80%;">HARE</span><span>L</span><span class="sl-logotype-small" style="font-size: 80%;">A</span><span>T</span><span class="sl-logotype-small" style="font-size: 80%;">E</span><span>X</span>
+								</h1>
+								</th>
+								<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
+							</tr></tbody></table>
+							</td></tr></tbody></table>
+						</td></tr></table>
+						<table class="spacer float-center" style="Margin: 0 auto; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+						<table align="center" class="container main float-center" style="Margin: 0 auto; Margin-top: 10px; background: #fefefe; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; margin-top: 10px; padding: 0; text-align: center; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+							<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+							
+							<%= body %>
+							
+							<table class="wrapper secondary" align="center" style="background: #f6f6f6; border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+								<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="10px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 10px; font-weight: normal; hyphens: auto; line-height: 10px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+								<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;"><small style="color: #7a7a7a; font-size: 80%;">
+									#{ settings.appName} &bull; <a href="#{ settings.siteUrl }" style="Margin: 0; color: #a93529; font-family: Helvetica, Arial, sans-serif; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; text-decoration: none;">#{ settings.siteUrl }</a>
+								</small></p>
+							</td></tr></table>
+						</td></tr></tbody></table>
+						
+					</center>
+				</td>
+			</tr>
+		</table>
+		<!-- prevent Gmail on iOS font size manipulation -->
+	 <div style="display:none; white-space:nowrap; font:15px courier; line-height:0;"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </div>
+	</body>
+</html>
+
+"""

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

@@ -244,17 +244,8 @@ module.exports = (app, webRouter, apiRouter)->
 		for key, value of Settings.nav
 		for key, value of Settings.nav
 			res.locals.nav[key] = _.clone(Settings.nav[key])
 			res.locals.nav[key] = _.clone(Settings.nav[key])
 		res.locals.templates = Settings.templateLinks
 		res.locals.templates = Settings.templateLinks
-		try
-			externalAuth = res.locals.externalAuthenticationSystemUsed()
-			if externalAuth and res.locals.nav.header?
-				# filter out '/register' link
-				res.locals.nav.header = _.filter(
-					res.locals.nav.header,
-					(h) ->
-						h.url != '/register'
-				)
-		catch error
-			logger.error {error}, "error while trying to filter out '/register' links from header"
+		if res.locals.nav.header
+			console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead"
 		next()
 		next()
 
 
 	webRouter.use (req, res, next) ->
 	webRouter.use (req, res, next) ->

+ 35 - 7
services/web/app/views/layout/navbar.jade

@@ -24,7 +24,10 @@ nav.navbar.navbar-default
 							li
 							li
 								a(href="/admin/user") Manage Users
 								a(href="/admin/user") Manage Users
 
 
-				each item in nav.header
+
+				// loop over header_extras
+				each item in nav.header_extras
+
 					if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
 					if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
 						if item.dropdown
 						if item.dropdown
 							li.dropdown(class=item.class, dropdown)
 							li.dropdown(class=item.class, dropdown)
@@ -35,9 +38,6 @@ nav.navbar.navbar-default
 									each child in item.dropdown
 									each child in item.dropdown
 										if child.divider
 										if child.divider
 											li.divider
 											li.divider
-										else if child.user_email
-											li
-												div.subdued #{getUserEmail()}
 										else
 										else
 											li
 											li
 												if child.url
 												if child.url
@@ -50,7 +50,35 @@ nav.navbar.navbar-default
 									a(href=item.url, class=item.class) !{translate(item.text)}
 									a(href=item.url, class=item.class) !{translate(item.text)}
 								else
 								else
 									| !{translate(item.text)}
 									| !{translate(item.text)}
-				
-				
-				
 
 
+				// logged out
+				if !getSessionUser()
+					// register link
+					if !externalAuthenticationSystemUsed()
+						li
+							a(href="/register") #{translate('register')}
+
+					// login link
+					li
+						a(href="/login") #{translate('log_in')}
+
+				// projects link and account menu
+				if getSessionUser()
+					li
+						a(href="/project") #{translate('Projects')}
+					li.dropdown(dropdown)
+						a.dropbodw-toggle(href, dropdown-toggle)
+							| #{translate('Account')}
+							b.caret
+						ul.dropdown-menu
+							li
+								div.subdued #{getUserEmail()}
+							li.divider
+							li
+								a(href="/user/settings") #{translate('Account Settings')}
+							if nav.showSubscriptionLink
+								li
+									a(href="/user/subscription") #{translate('subscription')}
+							li.divider
+							li
+								a(href="/logout") #{translate('log_out')}

+ 2 - 0
services/web/app/views/project/editor/share.jade

@@ -144,6 +144,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 				span(ng-switch="state.errorReason")
 				span(ng-switch="state.errorReason")
 					span(ng-switch-when="cannot_invite_non_user")
 					span(ng-switch-when="cannot_invite_non_user")
 						| #{translate("cannot_invite_non_user")}
 						| #{translate("cannot_invite_non_user")}
+					span(ng-switch-when="cannot_invite_self")
+						| #{translate("cannot_invite_self")}
 					span(ng-switch-default)
 					span(ng-switch-default)
 						| #{translate("generic_something_went_wrong")}
 						| #{translate("generic_something_went_wrong")}
 		button.btn.btn-default(
 		button.btn.btn-default(

+ 5 - 29
services/web/config/settings.defaults.coffee

@@ -335,35 +335,11 @@ module.exports = settings =
 			url: "https://github.com/sharelatex/sharelatex"
 			url: "https://github.com/sharelatex/sharelatex"
 		}]
 		}]
 
 
-		header: [{
-			text: "register"
-			url: "/register"
-			only_when_logged_out: true
-		}, {
-			text: "log_in"
-			url: "/login"
-			only_when_logged_out: true
-		}, {
-			text: "Projects"
-			url: "/project"
-			only_when_logged_in: true
-		}, {
-			text: "Account"
-			only_when_logged_in: true
-			dropdown: [{
-				user_email: true
-			},{
-				divider: true
-			}, {
-				text: "Account Settings"
-				url: "/user/settings"
-			}, {
-				divider: true
-			}, {
-				text: "Log out"
-				url: "/logout"
-			}]
-		}]
+		showSubscriptionLink: false
+
+		header_extras: []
+		# Example:
+		#   header_extras: [{text: "Some Page", url: "http://example.com/some/page", class: "subdued"}]
 
 
 	customisation: {}
 	customisation: {}
 
 

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

@@ -242,6 +242,10 @@ var createLatexWorker = function (session) {
         var annotations = [];
         var annotations = [];
         var newRange = {};
         var newRange = {};
         var cursor = selection.getCursor();
         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 = [];
         suppressions = [];
 
 
         for (var i = 0, len = hints.length; i<len; i++) {
         for (var i = 0, len = hints.length; i<len; i++) {
@@ -250,8 +254,8 @@ var createLatexWorker = function (session) {
             var suppressedChanges = 0;
             var suppressedChanges = 0;
             var hintRange = new Range(hint.start_row, hint.start_col, hint.end_row, hint.end_col);
             var hintRange = new Range(hint.start_row, hint.start_col, hint.end_row, hint.end_col);
 
 
-            var cursorInRange = hintRange.insideStart(cursor.row, cursor.column);
-            var cursorAtStart = hintRange.isStart(cursor.row, cursor.column);
+            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);
             var cursorAtEnd = hintRange.isEnd(cursor.row, cursor.column);
             if (hint.suppressIfEditing && (cursorAtStart || cursorAtEnd)) {
             if (hint.suppressIfEditing && (cursorAtStart || cursorAtEnd)) {
                 suppressions.push(hintRange);
                 suppressions.push(hintRange);
@@ -289,8 +293,10 @@ var createLatexWorker = function (session) {
                 cursorInRange = newRange[key].cursorInRange;
                 cursorInRange = newRange[key].cursorInRange;
                 hint = newRange[key].hint;
                 hint = newRange[key].hint;
                 var errorAtStart = (hint.row === hint.start_row && hint.column === hint.start_col);
                 var errorAtStart = (hint.row === hint.start_row && hint.column === hint.start_col);
-                var a = (cursorInRange && !errorAtStart) ? cursorAnchor : doc.createAnchor(new_range.start);
-                var b = (cursorInRange && errorAtStart) ? cursorAnchor : doc.createAnchor(new_range.end);
+                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();
                 var range = new Range();
                 range.start = a;
                 range.start = a;
                 range.end = b;
                 range.end = b;

+ 565 - 112
services/web/public/js/ace-1.2.5/worker-latex.js

@@ -1490,7 +1490,7 @@ var Tokenise = function (text) {
             var controlSequence = NEXTCS.exec(text);
             var controlSequence = NEXTCS.exec(text);
             var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
             var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
             if (nextSpecialPos === idx) {
             if (nextSpecialPos === idx) {
-                Tokens.push([lineNumber, code, pos, idx + 1, text[idx]]);
+                Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]);
                 idx = SPECIAL.lastIndex = idx + 1;
                 idx = SPECIAL.lastIndex = idx + 1;
                 char = text[nextSpecialPos];
                 char = text[nextSpecialPos];
                 if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
                 if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
@@ -1508,12 +1508,7 @@ var Tokenise = function (text) {
         } else if (code === "}") {  // close group
         } else if (code === "}") {  // close group
             Tokens.push([lineNumber, code, pos]);
             Tokens.push([lineNumber, code, pos]);
         } else if (code === "$") {  // math mode
         } else if (code === "$") {  // math mode
-            if (text[idx] === "$") {
-                idx = SPECIAL.lastIndex = idx + 1;
-                Tokens.push([lineNumber, "$$", pos]);
-            } else {
-                Tokens.push([lineNumber, code, pos]);
-            }
+            Tokens.push([lineNumber, code, pos]);
         } else if (code === "&") {  // tabalign
         } else if (code === "&") {  // tabalign
             Tokens.push([lineNumber, code, pos]);
             Tokens.push([lineNumber, code, pos]);
         } else if (code === "#") {  // macro parameter
         } else if (code === "#") {  // macro parameter
@@ -1559,6 +1554,25 @@ var read1arg = function (TokeniseResult, k, options) {
     }
     }
 };
 };
 
 
+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 read1name = function (TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
     var Tokens = TokeniseResult.tokens;
@@ -1594,19 +1608,88 @@ var read1name = function (TokeniseResult, k) {
     }
     }
 };
 };
 
 
+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 readOptionalParams = function(TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
     var Tokens = TokeniseResult.tokens;
     var text = TokeniseResult.text;
     var text = TokeniseResult.text;
 
 
     var params = Tokens[k+1];
     var params = Tokens[k+1];
-
     if(params && params[1] === "Text") {
     if(params && params[1] === "Text") {
         var paramNum = text.substring(params[2], params[3]);
         var paramNum = text.substring(params[2], params[3]);
         if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
         if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
             return k + 1; // got it
             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;
     return null;
+
 };
 };
 
 
 var readDefinition = function(TokeniseResult, k) {
 var readDefinition = function(TokeniseResult, k) {
@@ -1697,7 +1780,6 @@ var readUrl = function(TokeniseResult, k) {
     return null;
     return null;
 };
 };
 
 
-
 var InterpretTokens = function (TokeniseResult, ErrorReporter) {
 var InterpretTokens = function (TokeniseResult, ErrorReporter) {
     var Tokens = TokeniseResult.tokens;
     var Tokens = TokeniseResult.tokens;
     var linePosition = TokeniseResult.linePosition;
     var linePosition = TokeniseResult.linePosition;
@@ -1706,11 +1788,30 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
 
 
     var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
     var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
     var TokenError = ErrorReporter.TokenError;
     var TokenError = ErrorReporter.TokenError;
-    var Environments = [];
+    var Environments = new EnvHandler(ErrorReporter);
+
+    var nextGroupMathMode = null; // if the next group should have
+    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++) {
     for (var i = 0, len = Tokens.length; i < len; i++) {
         var token = Tokens[i];
         var token = Tokens[i];
         var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
         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 (type === "\\") {
             if (seq === "begin" || seq === "end") {
             if (seq === "begin" || seq === "end") {
                 var open = Tokens[i+1];
                 var open = Tokens[i+1];
@@ -1759,15 +1860,31 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                     } else {
                     } else {
                         TokenError(token, "invalid environment command");
                         TokenError(token, "invalid environment command");
                     };
                     };
-                    }
-            } else if (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") {
-                var newPos = read1arg(TokeniseResult, i, {allowStar: (seq != "def")});
+                }
+            } 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;};
                 if (newPos === null) { continue; } else {i = newPos;};
                 newPos = readOptionalParams(TokeniseResult, i);
                 newPos = readOptionalParams(TokeniseResult, i);
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 newPos = readDefinition(TokeniseResult, i);
                 newPos = readDefinition(TokeniseResult, i);
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 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") {
             } else if (seq === "newcolumntype") {
                 newPos = read1name(TokeniseResult, i);
                 newPos = read1name(TokeniseResult, i);
                 if (newPos === null) { continue; } else {i = newPos;};
                 if (newPos === null) { continue; } else {i = newPos;};
@@ -1791,128 +1908,435 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             } else if (seq === "url") {
             } else if (seq === "url") {
                 newPos = readUrl(TokeniseResult, i);
                 newPos = readUrl(TokeniseResult, i);
                 if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;};
                 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 === "{") {
-            Environments.push({command:"{", token:token});
-        } else if (type === "}") {
-            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;
     return Environments;
 };
 };
 
 
-
-var CheckEnvironments = function (Environments, ErrorReporter) {
+var EnvHandler = function (ErrorReporter) {
     var ErrorTo = ErrorReporter.EnvErrorTo;
     var ErrorTo = ErrorReporter.EnvErrorTo;
     var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
     var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
     var ErrorFrom = ErrorReporter.EnvErrorFrom;
     var ErrorFrom = ErrorReporter.EnvErrorFrom;
 
 
+    var envs = [];
+
     var state = [];
     var state = [];
     var documentClosed = null;
     var documentClosed = null;
     var inVerbatim = false;
     var inVerbatim = false;
     var verbatimRanges = [];
     var verbatimRanges = [];
-    for (var i = 0, len = Environments.length; i < len; i++) {
-        var name = Environments[i].name ;
-        if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted)$/)) {
-            Environments[i].verbatim = true;
+
+    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); } ;
         }
         }
-    }
-    for (i = 0, len = Environments.length; i < len; i++) {
-        var thisEnv = Environments[i];
-        if(thisEnv.command === "begin" || thisEnv.command === "{") {
-            if (inVerbatim) { continue; } // ignore anything in verbatim environments
-            if (thisEnv.verbatim) {inVerbatim = true;};
-            state.push(thisEnv);
-        } else if (thisEnv.command === "end" || thisEnv.command === "}") {
-            var lastEnv = state.pop();
+    };
 
 
-            if (inVerbatim) {
-                if (lastEnv && lastEnv.name === thisEnv.name) {
-                    inVerbatim = false;
-                    verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
-                    continue;
-                } else {
-                    if(lastEnv) { state.push(lastEnv); } ;
-                    continue;  // ignore all other commands
-                }
-            };
+    var invalidEnvs = [];
+
+    this._end = function (thisEnv) {
+        do {
+            var lastEnv = state.pop();
+            var retry = false;
+            var i;
 
 
-            if (lastEnv && lastEnv.command === "{" && thisEnv.command === "}") {
-                continue;
-            } else if (lastEnv && lastEnv.name === thisEnv.name) {
-                if (thisEnv.name === "document" && !documentClosed) {
+            if (closedBy(lastEnv, thisEnv)) {
+                if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
                     documentClosed = thisEnv;
                     documentClosed = thisEnv;
                 };
                 };
-                continue;
+                return;
             } else if (!lastEnv) {
             } else if (!lastEnv) {
-                if (thisEnv.command === "}") {
-                    if (documentClosed) {
-                        ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
-                    } else {
-                        ErrorTo(thisEnv, "unexpected end group }");
-                    };
-                } else if (thisEnv.command === "end") {
-                    if (documentClosed) {
-                        ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
-                    } else {
-                        ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
-                    }
-                }
-            } else if (lastEnv.command === "begin" && thisEnv.command === "}") {
-                ErrorFromTo(lastEnv, thisEnv, "unexpected end group } after \\begin{" + lastEnv.name +"}");
-                state.push(lastEnv);
-            } else if (lastEnv.command === "{" && thisEnv.command === "end") {
-                ErrorFromTo(lastEnv, thisEnv,
-                            "unclosed group { found at \\end{" + thisEnv.name + "}",
-                            {suppressIfEditing:true, errorAtStart: true, type:"warning"});
-                i--;
-            } else if (lastEnv.command === "begin" && thisEnv.command === "end") {
-                ErrorFromTo(lastEnv, thisEnv,
-                            "unclosed \\begin{" + lastEnv.name + "} found at \\end{" + thisEnv.name + "} " ,
-                            {errorAtStart: true});
-                for (var j = i + 1; j < len; j++) {
-                    var futureEnv = Environments[j];
-                    if (futureEnv.command === "end" && futureEnv.name === lastEnv.name) {
-                        state.push(lastEnv);
-                        continue;
-                    }
+                if (documentClosed) {
+                    ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+                } else {
+                    ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
                 }
                 }
-                lastEnv = state.pop();
-                if(lastEnv) {
-                    if (thisEnv.name === lastEnv.name) {
-                        continue;
-                    } else {
-                        state.push(lastEnv);
+            } 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;
+            }
         }
         }
-    }
-    while (state.length > 0) {
-        thisEnv = state.pop();
+        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.command === "{") {
-            ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
+            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") {
         } else if (thisEnv.command === "begin") {
-            ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
+            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
+                }
+            }
         };
         };
-    }
-    var vlen = verbatimRanges.length;
-    len = ErrorReporter.tokenErrors.length;
-    if (vlen >0 && len > 0) {
-        for (i = 0; i < len; i++) {
-            var tokenError = ErrorReporter.tokenErrors[i];
-            var startPos = tokenError.startPos;
-            var endPos = tokenError.endPos;
-            for (j = 0; j < vlen; j++) {
-                if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
-                    tokenError.ignore = true;
-                    break;
+        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 ErrorReporter = function (TokeniseResult) {
     var text = TokeniseResult.text;
     var text = TokeniseResult.text;
@@ -1922,18 +2346,41 @@ var ErrorReporter = function (TokeniseResult) {
     var errors = [], tokenErrors = [];
     var errors = [], tokenErrors = [];
     this.errors = errors;
     this.errors = errors;
     this.tokenErrors = tokenErrors;
     this.tokenErrors = tokenErrors;
+    this.filterMath = false;
 
 
     this.getErrors = function () {
     this.getErrors = function () {
         var returnedErrors = [];
         var returnedErrors = [];
         for (var i = 0, len = tokenErrors.length; i < len; i++) {
         for (var i = 0, len = tokenErrors.length; i < len; i++) {
             if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
             if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
         }
         }
-        return returnedErrors.concat(errors);
+        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) {
+    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 line = token[0], type = token[1], start = token[2], end = token[3];
         var start_col = start - linePosition[line];
         var start_col = start - linePosition[line];
+        if (!end) { end = start + 1; } ;
         var end_col = end - linePosition[line];
         var end_col = end - linePosition[line];
         tokenErrors.push({row: line,
         tokenErrors.push({row: line,
                           column: start_col,
                           column: start_col,
@@ -1945,10 +2392,12 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           text:message,
                           startPos: start,
                           startPos: start,
                           endPos: end,
                           endPos: end,
-                          suppressIfEditing:true});
+                          suppressIfEditing:options.suppressIfEditing,
+                          mathMode: options.mathMode});
     };
     };
 
 
-    this.TokenErrorFromTo = function (fromToken, toToken, message) {
+    this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
+        if(!options) { options = {suppressIfEditing:true } ; };
         var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
         var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
         var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
         var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
         if (!toEnd) { toEnd = toStart + 1;};
         if (!toEnd) { toEnd = toStart + 1;};
@@ -1965,7 +2414,8 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           text:message,
                           startPos: fromStart,
                           startPos: fromStart,
                           endPos: toEnd,
                           endPos: toEnd,
-                          suppressIfEditing:true});
+                          suppressIfEditing:options.suppressIfEditing,
+                          mathMode: options.mathMode});
     };
     };
 
 
 
 
@@ -1986,7 +2436,8 @@ var ErrorReporter = function (TokeniseResult) {
                      end_col: end_col,
                      end_col: end_col,
                      type: options.type ? options.type : "error",
                      type: options.type ? options.type : "error",
                      text:message,
                      text:message,
-                     suppressIfEditing:options.suppressIfEditing});
+                     suppressIfEditing:options.suppressIfEditing,
+                     mathMode: options.mathMode});
     };
     };
 
 
     this.EnvErrorTo = function (toEnv, message, options) {
     this.EnvErrorTo = function (toEnv, message, options) {
@@ -2002,7 +2453,8 @@ var ErrorReporter = function (TokeniseResult) {
                    end_row: line,
                    end_row: line,
                    end_col: end_col,
                    end_col: end_col,
                    type: options.type ? options.type : "error",
                    type: options.type ? options.type : "error",
-                   text:message};
+                   text:message,
+                   mathMode: options.mathMode};
         errors.push(err);
         errors.push(err);
     };
     };
 
 
@@ -2019,7 +2471,8 @@ var ErrorReporter = function (TokeniseResult) {
                      end_row: lineNumber,
                      end_row: lineNumber,
                      end_col: end_col,
                      end_col: end_col,
                      type: options.type ? options.type : "error",
                      type: options.type ? options.type : "error",
-                     text:message});
+                     text:message,
+                     mathMode: options.mathMode});
     };
     };
 };
 };
 
 
@@ -2027,7 +2480,7 @@ var Parse = function (text) {
     var TokeniseResult = Tokenise(text);
     var TokeniseResult = Tokenise(text);
     var Reporter = new ErrorReporter(TokeniseResult);
     var Reporter = new ErrorReporter(TokeniseResult);
     var Environments = InterpretTokens(TokeniseResult, Reporter);
     var Environments = InterpretTokens(TokeniseResult, Reporter);
-    CheckEnvironments(Environments, Reporter);
+    Environments.close();
     return Reporter.getErrors();
     return Reporter.getErrors();
 };
 };
 
 

+ 188 - 33
services/web/public/js/ace-1.2.5/worker-latex_beta.js

@@ -1554,6 +1554,25 @@ var read1arg = function (TokeniseResult, k, options) {
     }
     }
 };
 };
 
 
+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 read1name = function (TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
     var Tokens = TokeniseResult.tokens;
@@ -1624,9 +1643,56 @@ var readOptionalParams = function(TokeniseResult, k) {
             return k + 1; // got it
             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;
     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 readDefinition = function(TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
     var Tokens = TokeniseResult.tokens;
     var text = TokeniseResult.text;
     var text = TokeniseResult.text;
@@ -1726,10 +1792,27 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
     var Environments = new EnvHandler(ErrorReporter);
     var Environments = new EnvHandler(ErrorReporter);
 
 
     var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
     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++) {
     for (var i = 0, len = Tokens.length; i < len; i++) {
         var token = Tokens[i];
         var token = Tokens[i];
         var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
         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 (type === "\\") {
             if (seq === "begin" || seq === "end") {
             if (seq === "begin" || seq === "end") {
                 var open = Tokens[i+1];
                 var open = Tokens[i+1];
@@ -1778,15 +1861,31 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                     } else {
                     } else {
                         TokenError(token, "invalid environment command");
                         TokenError(token, "invalid environment command");
                     };
                     };
-                    }
-            } else if (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") {
-                var newPos = read1arg(TokeniseResult, i, {allowStar: (seq != "def")});
+                }
+            } 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;};
                 if (newPos === null) { continue; } else {i = newPos;};
                 newPos = readOptionalParams(TokeniseResult, i);
                 newPos = readOptionalParams(TokeniseResult, i);
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 newPos = readDefinition(TokeniseResult, i);
                 newPos = readDefinition(TokeniseResult, i);
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 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") {
             } else if (seq === "newcolumntype") {
                 newPos = read1name(TokeniseResult, i);
                 newPos = read1name(TokeniseResult, i);
                 if (newPos === null) { continue; } else {i = newPos;};
                 if (newPos === null) { continue; } else {i = newPos;};
@@ -1820,7 +1919,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                 } else if (nextToken && nextToken[1] === "\\") {
                 } else if (nextToken && nextToken[1] === "\\") {
                     char = "unknown";
                     char = "unknown";
                 }
                 }
-                if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) {
+                if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) {
                     TokenError(token, "invalid bracket command");
                     TokenError(token, "invalid bracket command");
                 } else {
                 } else {
                     i = i + 1;
                     i = i + 1;
@@ -1831,25 +1930,50 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             } else if (seq === "input") {
             } else if (seq === "input") {
                 newPos = read1filename(TokeniseResult, i);
                 newPos = read1filename(TokeniseResult, i);
                 if (newPos === null) { continue; } else {i = newPos;};
                 if (newPos === null) { continue; } else {i = newPos;};
-            } else if (seq === "hbox" || seq === "text" || seq === "mbox") {
+            } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") {
                 nextGroupMathMode = false;
                 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)$/)) {
             } 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)
                 var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode === null && !insideGroup) {
-                    TokenError(token, type + seq + " must be inside math mode");
+                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|cite|ref)/)) {
+            } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) {
                 currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
                 currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode && !insideGroup) {
-                    TokenError(token, type + seq + " used inside math mode");
+                if (currentMathMode) {
+                    TokenError(token, type + seq + " used inside math mode", {mathMode:true});
                     Environments.resetMathMode();
                     Environments.resetMathMode();
                 };
                 };
+            } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) {
+                nextGroupMathMode = undefined;
             };
             };
-        } else if (type === "{") {
-            Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
-            nextGroupMathMode = null;
-        } else if (type === "}") {
-            Environments.push({command:"}", token:token});
+
         } else if (type === "$") {
         } else if (type === "$") {
             var lookAhead = Tokens[i+1];
             var lookAhead = Tokens[i+1];
             var nextIsDollar = lookAhead && lookAhead[1] === "$";
             var nextIsDollar = lookAhead && lookAhead[1] === "$";
@@ -1864,12 +1988,15 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
             currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
             var insideGroup = Environments.insideGroup();  // true if inside {....}
             var insideGroup = Environments.insideGroup();  // true if inside {....}
             if (currentMathMode === null && !insideGroup) {
             if (currentMathMode === null && !insideGroup) {
-                TokenError(token, type + " must be inside math mode");
+                TokenError(token, type + " must be inside math mode", {mathMode:true});
             };
             };
-        } else {
-            nextGroupMathMode = null;
         }
         }
     };
     };
+
+    if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) {
+        ErrorReporter.filterMath = true;
+    };
+
     return Environments;
     return Environments;
 };
 };
 
 
@@ -1920,7 +2047,7 @@ var EnvHandler = function (ErrorReporter) {
                 if (documentClosed) {
                 if (documentClosed) {
                     ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
                     ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
                 } else {
                 } else {
-                    ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
+                    ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
                 }
                 }
             } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
             } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
                 invalidEnvs.splice(i, 1);
                 invalidEnvs.splice(i, 1);
@@ -2054,7 +2181,7 @@ var EnvHandler = function (ErrorReporter) {
         var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
         var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
         if (currentMathMode) {
         if (currentMathMode) {
             ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
             ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
-                      {suppressIfEditing:true, errorAtStart: true});
+                      {suppressIfEditing:true, errorAtStart: true, mathMode:true});
         };
         };
         thisEnv.mathMode = thisEnv;
         thisEnv.mathMode = thisEnv;
         state.push(thisEnv);
         state.push(thisEnv);
@@ -2118,28 +2245,28 @@ var EnvHandler = function (ErrorReporter) {
             }
             }
         } else if (thisEnv.command === "left") {
         } else if (thisEnv.command === "left") {
             if (currentMathMode === null) {
             if (currentMathMode === null) {
-                ErrorFrom(thisEnv, "\\left can only be used in math mode");
+                ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true});
             };
             };
             newMathMode = currentMathMode;
             newMathMode = currentMathMode;
         } else if (thisEnv.command === "begin") {
         } else if (thisEnv.command === "begin") {
             var name = thisEnv.name;
             var name = thisEnv.name;
             if (name) {
             if (name) {
-                if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
+                if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
                     if (currentMathMode) {
                     if (currentMathMode) {
                         ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
                         ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true});
+                                    {suppressIfEditing:true, errorAtStart: true, mathMode: true});
                         resetMathMode();
                         resetMathMode();
                     };
                     };
                     newMathMode = null;
                     newMathMode = null;
-                } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) {
-                    if (!currentMathMode) {
-                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode");
+                } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) {
+                    if (currentMathMode === null) {
+                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true});
                     };
                     };
                     newMathMode = currentMathMode;
                     newMathMode = currentMathMode;
                 } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
                 } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
                     if (currentMathMode) {
                     if (currentMathMode) {
                         ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
                         ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true});
+                                    {suppressIfEditing:true, errorAtStart: true, mathMode: true});
                         resetMathMode();
                         resetMathMode();
                     };
                     };
                     newMathMode = thisEnv;
                     newMathMode = thisEnv;
@@ -2220,13 +2347,36 @@ var ErrorReporter = function (TokeniseResult) {
     var errors = [], tokenErrors = [];
     var errors = [], tokenErrors = [];
     this.errors = errors;
     this.errors = errors;
     this.tokenErrors = tokenErrors;
     this.tokenErrors = tokenErrors;
+    this.filterMath = false;
 
 
     this.getErrors = function () {
     this.getErrors = function () {
         var returnedErrors = [];
         var returnedErrors = [];
         for (var i = 0, len = tokenErrors.length; i < len; i++) {
         for (var i = 0, len = tokenErrors.length; i < len; i++) {
             if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
             if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
         }
         }
-        return returnedErrors.concat(errors);
+        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) {
     this.TokenError = function (token, message, options) {
@@ -2245,7 +2395,8 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           text:message,
                           startPos: start,
                           startPos: start,
                           endPos: end,
                           endPos: end,
-                          suppressIfEditing:options.suppressIfEditing});
+                          suppressIfEditing:options.suppressIfEditing,
+                          mathMode: options.mathMode});
     };
     };
 
 
     this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
     this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
@@ -2266,7 +2417,8 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           text:message,
                           startPos: fromStart,
                           startPos: fromStart,
                           endPos: toEnd,
                           endPos: toEnd,
-                          suppressIfEditing:options.suppressIfEditing});
+                          suppressIfEditing:options.suppressIfEditing,
+                          mathMode: options.mathMode});
     };
     };
 
 
 
 
@@ -2287,7 +2439,8 @@ var ErrorReporter = function (TokeniseResult) {
                      end_col: end_col,
                      end_col: end_col,
                      type: options.type ? options.type : "error",
                      type: options.type ? options.type : "error",
                      text:message,
                      text:message,
-                     suppressIfEditing:options.suppressIfEditing});
+                     suppressIfEditing:options.suppressIfEditing,
+                     mathMode: options.mathMode});
     };
     };
 
 
     this.EnvErrorTo = function (toEnv, message, options) {
     this.EnvErrorTo = function (toEnv, message, options) {
@@ -2303,7 +2456,8 @@ var ErrorReporter = function (TokeniseResult) {
                    end_row: line,
                    end_row: line,
                    end_col: end_col,
                    end_col: end_col,
                    type: options.type ? options.type : "error",
                    type: options.type ? options.type : "error",
-                   text:message};
+                   text:message,
+                   mathMode: options.mathMode};
         errors.push(err);
         errors.push(err);
     };
     };
 
 
@@ -2320,7 +2474,8 @@ var ErrorReporter = function (TokeniseResult) {
                      end_row: lineNumber,
                      end_row: lineNumber,
                      end_col: end_col,
                      end_col: end_col,
                      type: options.type ? options.type : "error",
                      type: options.type ? options.type : "error",
-                     text:message});
+                     text:message,
+                     mathMode: options.mathMode});
     };
     };
 };
 };
 
 

+ 7 - 0
services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee

@@ -387,6 +387,10 @@ describe "AuthenticationController", ->
 		beforeEach ->
 		beforeEach ->
 			@req.headers = {}
 			@req.headers = {}
 			@AuthenticationController.httpAuth = sinon.stub()
 			@AuthenticationController.httpAuth = sinon.stub()
+			@_setRedirect = sinon.spy(@AuthenticationController, '_setRedirectInSession')
+
+		afterEach ->
+			@_setRedirect.restore()
 
 
 		describe "with white listed url", ->
 		describe "with white listed url", ->
 			beforeEach ->
 			beforeEach ->
@@ -431,6 +435,9 @@ describe "AuthenticationController", ->
 				@req.session = {}
 				@req.session = {}
 				@AuthenticationController.requireGlobalLogin @req, @res, @next
 				@AuthenticationController.requireGlobalLogin @req, @res, @next
 
 
+			it 'should have called setRedirectInSession', ->
+				@_setRedirect.callCount.should.equal 1
+
 			it "should redirect to the /login page", ->
 			it "should redirect to the /login page", ->
 				@res.redirectedTo.should.equal "/login"
 				@res.redirectedTo.should.equal "/login"
 
 

+ 31 - 0
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee

@@ -255,6 +255,37 @@ describe "CollaboratorsInviteController", ->
 			it 'should not have called inviteToProject', ->
 			it 'should not have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
 
 
+		describe 'when the user invites themselves to the project', ->
+
+			beforeEach ->
+				@req.session.user = {_id: 'abc', email: 'me@example.com'}
+				@req.body.email = 'me@example.com'
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, null, true)
+				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
+			it 'should reject action, return json response with error code', ->
+				@res.json.callCount.should.equal 1
+				({invite: null, error: 'cannot_invite_self'}).should.deep.equal(@res.json.firstCall.args[0])
+
+			it 'should not have called canAddXCollaborators', ->
+				@LimitationsManager.canAddXCollaborators.callCount.should.equal 0
+
+			it 'should not have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 0
+
+			it 'should not have called inviteToProject', ->
+				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
+			it 'should not have called emitToRoom', ->
+				@EditorRealTimeController.emitToRoom.callCount.should.equal 0
+
+
 	describe "viewInvite", ->
 	describe "viewInvite", ->
 
 
 		beforeEach ->
 		beforeEach ->

+ 2 - 2
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee

@@ -185,7 +185,7 @@ describe "CollaboratorsInviteHandler", ->
 	describe '_sendMessages', ->
 	describe '_sendMessages', ->
 
 
 		beforeEach ->
 		beforeEach ->
-			@CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, null)
+			@CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, null)
 			@CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null)
 			@CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null)
 			@call = (callback) =>
 			@call = (callback) =>
 				@CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback
 				@CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback
@@ -213,7 +213,7 @@ describe "CollaboratorsInviteHandler", ->
 		describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', ->
 		describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, new Error('woops'))
+				@CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, new Error('woops'))
 
 
 			it 'should produce an error', (done) ->
 			it 'should produce an error', (done) ->
 				@call (err, invite) =>
 				@call (err, invite) =>

+ 30 - 0
services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee

@@ -10,6 +10,9 @@ describe "EmailSender", ->
 
 
 	beforeEach ->
 	beforeEach ->
 
 
+		@RateLimiter =
+			addCount:sinon.stub()
+
 		@settings =
 		@settings =
 			email:
 			email:
 				transport: "ses"
 				transport: "ses"
@@ -21,11 +24,15 @@ describe "EmailSender", ->
 
 
 		@sesClient =
 		@sesClient =
 			sendMail: sinon.stub()
 			sendMail: sinon.stub()
+
 		@ses =
 		@ses =
 			createTransport: => @sesClient
 			createTransport: => @sesClient
+
+
 		@sender = SandboxedModule.require modulePath, requires:
 		@sender = SandboxedModule.require modulePath, requires:
 			'nodemailer': @ses
 			'nodemailer': @ses
 			"settings-sharelatex":@settings
 			"settings-sharelatex":@settings
+			'../../infrastructure/RateLimiter':@RateLimiter
 			"logger-sharelatex":
 			"logger-sharelatex":
 				log:->
 				log:->
 				warn:->
 				warn:->
@@ -84,6 +91,29 @@ describe "EmailSender", ->
 				args.replyTo.should.equal @opts.replyTo
 				args.replyTo.should.equal @opts.replyTo
 				done()
 				done()
 
 
+
+		it "should not send an email when the rate limiter says no", (done)->
+			@opts.sendingUser_id = "12321312321"
+			@RateLimiter.addCount.callsArgWith(1, null, false)
+			@sender.sendEmail @opts, =>
+				@sesClient.sendMail.called.should.equal false
+				done()
+
+		it "should send the email when the rate limtier says continue",  (done)->
+			@sesClient.sendMail.callsArgWith(1)
+			@opts.sendingUser_id = "12321312321"
+			@RateLimiter.addCount.callsArgWith(1, null, true)
+			@sender.sendEmail @opts, =>
+				@sesClient.sendMail.called.should.equal true
+				done()
+
+		it "should not check the rate limiter when there is no sendingUser_id", (done)->
+			@sesClient.sendMail.callsArgWith(1)
+			@sender.sendEmail @opts, =>
+				@sesClient.sendMail.called.should.equal true
+				@RateLimiter.addCount.called.should.equal false
+				done()
+
 		describe 'with plain-text email content', () ->
 		describe 'with plain-text email content', () ->
 
 
 			beforeEach ->
 			beforeEach ->