diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index e406296730..e8e3db4f93 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -148,6 +148,7 @@ module.exports = AuthenticationController =
return next()
else
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
+ AuthenticationController._setRedirectInSession(req)
return res.redirect "/login"
httpAuth: basicAuth (user, pass)->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index bc7eb90c3f..913562f417 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -11,7 +11,7 @@ module.exports = CollaboratorsEmailHandler =
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
- notifyUserOfProjectInvite: (project_id, email, invite, callback)->
+ notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
@@ -24,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
+ sendingUser_id: sendingUser._id
EmailHandler.sendEmail "projectInvite", emailOptions, callback
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 9d9f4d2a5e..460b62da1d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -37,6 +37,9 @@ module.exports = CollaboratorsInviteController =
email = req.body.email
sendingUser = AuthenticationController.getSessionUser(req)
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"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 5ed6570c3a..0e6cd8876c 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
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?
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
return callback(err) if err?
diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee
new file mode 100644
index 0000000000..00a878c276
--- /dev/null
+++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee
@@ -0,0 +1,49 @@
+_ = require("underscore")
+settings = require "settings-sharelatex"
+
+module.exports = _.template """
+
+
+
+ <%= title %>
+
+
+
+ <%= greeting %>
+
+
+ <%= message %>
+
+
+
+
+
+ <% if (secondaryMessage) { %>
+
+
+ <%= secondaryMessage %>
+
+ <% } %>
+ |
+ |
|---|
|
+
+<% if (gmailGoToAction) { %>
+
+<% } %>
+"""
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
index 70d11e219b..306aad3d2a 100644
--- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -1,6 +1,12 @@
_ = require('underscore')
+
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
+BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout")
+
+SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
+
+
settings = require("settings-sharelatex")
@@ -61,7 +67,7 @@ ShareLaTeX Co-founder
templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
- layout: NotificationEmailLayout
+ layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Password Reset
@@ -78,36 +84,21 @@ Thank you
#{settings.appName} - <%= siteUrl %>
"""
- compiledTemplate: _.template """
-Password Reset
-
-We got a request to reset your #{settings.appName} password.
-
-
-
-
-
-If you ignore this message, your password won't be changed.
-
-If you didn't request a password reset, let us know.
-
-
-Thank you
- #{settings.appName}
-"""
+ 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.
If you didn't request a password reset, let us know."
+ ctaText: "Reset password"
+ ctaURL: opts.setNewPasswordUrl
+ gmailGoToAction: null
+ })
templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
- layout: NotificationEmailLayout
+ layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
@@ -118,23 +109,25 @@ Thank you
#{settings.appName} - <%= siteUrl %>
"""
- compiledTemplate: _.template """
-Hi, <%= owner.email %> wants to share '<%= project.name %>' with you
-
-
-
- View Project
-
-
-
- Thank you
- #{settings.appName}
-"""
+ compiledTemplate: (opts) ->
+ SingleCTAEmailBody({
+ title: "#{ opts.project.name } – shared by #{ opts.owner.email }"
+ greeting: "Hi,"
+ message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” 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 =
subject: _.template "Verify Email to join <%= group_name %> group"
- layout: NotificationEmailLayout
+ layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
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 %>
"""
- compiledTemplate: _.template """
-Hi, please verify your email to join the <%= group_name %> and get your free premium account
-
-
-
- Thank you
- #{settings.appName}
-"""
-
+ 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 =
templates: templates
@@ -177,4 +163,4 @@ module.exports =
html: template.layout(opts)
text: template?.plainTextTemplate?(opts)
type:template.type
- }
+ }
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee
index a7bcc82ed7..7a909e083e 100644
--- a/services/web/app/coffee/Features/Email/EmailSender.coffee
+++ b/services/web/app/coffee/Features/Email/EmailSender.coffee
@@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport')
sgTransport = require('nodemailer-sendgrid-transport')
-
+rateLimiter = require('../../infrastructure/RateLimiter')
_ = require("underscore")
if Settings.email? and Settings.email.fromAddress?
@@ -39,24 +39,39 @@ if nm_client?
else
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 =
sendEmail : (options, callback = (error) ->)->
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?
- 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)
diff --git a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee
new file mode 100644
index 0000000000..6d25df2197
--- /dev/null
+++ b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee
@@ -0,0 +1,380 @@
+_ = require("underscore")
+settings = require "settings-sharelatex"
+
+module.exports = _.template """
+
+
+
+
+
+
+
+
+ Project invite
+
+
+
+
+
+
+
+
+
+
+"""
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index d976a1d23a..867583468b 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -244,17 +244,8 @@ module.exports = (app, webRouter, apiRouter)->
for key, value of Settings.nav
res.locals.nav[key] = _.clone(Settings.nav[key])
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()
webRouter.use (req, res, next) ->
diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade
index 3cd6587592..4d78d02fba 100644
--- a/services/web/app/views/layout/navbar.jade
+++ b/services/web/app/views/layout/navbar.jade
@@ -24,7 +24,10 @@ nav.navbar.navbar-default
li
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.dropdown
li.dropdown(class=item.class, dropdown)
@@ -35,9 +38,6 @@ nav.navbar.navbar-default
each child in item.dropdown
if child.divider
li.divider
- else if child.user_email
- li
- div.subdued #{getUserEmail()}
else
li
if child.url
@@ -50,7 +50,35 @@ nav.navbar.navbar-default
a(href=item.url, class=item.class) !{translate(item.text)}
else
| !{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')}
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index 62de414064..78fb69c333 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/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-when="cannot_invite_non_user")
| #{translate("cannot_invite_non_user")}
+ span(ng-switch-when="cannot_invite_self")
+ | #{translate("cannot_invite_self")}
span(ng-switch-default)
| #{translate("generic_something_went_wrong")}
button.btn.btn-default(
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index 708892e2fa..44e4a75867 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -335,35 +335,11 @@ module.exports = settings =
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: {}
diff --git a/services/web/public/js/ace-1.2.5/mode-latex.js b/services/web/public/js/ace-1.2.5/mode-latex.js
index f183d7c263..8e7bbe4802 100644
--- a/services/web/public/js/ace-1.2.5/mode-latex.js
+++ b/services/web/public/js/ace-1.2.5/mode-latex.js
@@ -242,6 +242,10 @@ var createLatexWorker = function (session) {
var annotations = [];
var newRange = {};
var cursor = selection.getCursor();
+ var maxRow = session.getLength() - 1;
+ var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0;
+ var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol);
+
suppressions = [];
for (var i = 0, len = hints.length; i 0) {
+ return j; // advance past these tokens
+ } else {
+ return null;
+ }
+};
+
var readOptionalParams = function(TokeniseResult, k) {
var Tokens = TokeniseResult.tokens;
var text = TokeniseResult.text;
+ var params = Tokens[k+1];
+ if(params && params[1] === "Text") {
+ var paramNum = text.substring(params[2], params[3]);
+ if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
+ return k + 1; // got it
+ };
+ };
+ var count = 0;
+ var nextToken = Tokens[k+1];
+ var pos = nextToken[2];
+
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === "[") { count++; }
+ if (char === "]") { count--; }
+ if (count === 0 && char === "{") { return k - 1; }
+ if (count > 0 && (char === '\r' || char === '\n')) { return null; }
+ };
+ return null;
+};
+
+var readOptionalGeneric = function(TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
var params = Tokens[k+1];
if(params && params[1] === "Text") {
var paramNum = text.substring(params[2], params[3]);
- if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
+ if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) {
return k + 1; // got it
};
};
return null;
};
+var readOptionalDef = function (TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var defToken = Tokens[k];
+ var pos = defToken[3];
+
+ var openBrace = "{";
+ var nextToken = Tokens[k+1];
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments
+ if (char === '\r' || char === '\n') { return null; }
+ };
+
+ return null;
+
+};
+
var readDefinition = function(TokeniseResult, k) {
var Tokens = TokeniseResult.tokens;
var text = TokeniseResult.text;
@@ -1697,7 +1780,6 @@ var readUrl = function(TokeniseResult, k) {
return null;
};
-
var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var Tokens = TokeniseResult.tokens;
var linePosition = TokeniseResult.linePosition;
@@ -1706,11 +1788,30 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
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++) {
var token = Tokens[i];
var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
+
+ if (type === "{") {
+ Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
+ nextGroupMathModeStack.push(nextGroupMathMode);
+ nextGroupMathMode = null;
+ continue;
+ } else if (type === "}") {
+ Environments.push({command:"}", token:token});
+ nextGroupMathMode = nextGroupMathModeStack.pop();
+ continue;
+ } else {
+ nextGroupMathMode = null;
+ };
+
if (type === "\\") {
if (seq === "begin" || seq === "end") {
var open = Tokens[i+1];
@@ -1759,15 +1860,31 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
} else {
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;};
newPos = readOptionalParams(TokeniseResult, i);
if (newPos === null) { /* do nothing */ } else {i = newPos;};
newPos = readDefinition(TokeniseResult, i);
if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (seq === "def") {
+ newPos = read1arg(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ newPos = readOptionalDef(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ } else if (seq === "let") {
+ newPos = readLetDefinition(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+
} else if (seq === "newcolumntype") {
newPos = read1name(TokeniseResult, i);
if (newPos === null) { continue; } else {i = newPos;};
@@ -1791,128 +1908,435 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
} else if (seq === "url") {
newPos = readUrl(TokeniseResult, i);
if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;};
+ } else if (seq === "left" || seq === "right") {
+ var nextToken = Tokens[i+1];
+ char = "";
+ if (nextToken && nextToken[1] === "Text") {
+ char = text.substring(nextToken[2], nextToken[2] + 1);
+ } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") {
+ char = nextToken[4];
+ } else if (nextToken && nextToken[1] === "\\") {
+ char = "unknown";
+ }
+ if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) {
+ TokenError(token, "invalid bracket command");
+ } else {
+ i = i + 1;
+ Environments.push({command:seq, token:token});
+ };
+ } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") {
+ Environments.push({command:seq, token:token});
+ } else if (seq === "input") {
+ newPos = read1filename(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") {
+ nextGroupMathMode = false;
+ } else if (seq === "rotatebox" || seq === "scalebox") {
+ newPos = readOptionalGeneric(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ nextGroupMathMode = false;
+ } else if (seq === "resizebox") {
+ newPos = readOptionalGeneric(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ nextGroupMathMode = false;
+ } else if (seq === "DeclareMathOperator") {
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (seq === "DeclarePairedDelimiter") {
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) {
+ var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ if (currentMathMode === null) {
+ TokenError(token, type + seq + " must be inside math mode", {mathMode:true});
+ };
+ } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) {
+ currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ if (currentMathMode) {
+ TokenError(token, type + seq + " used inside math mode", {mathMode:true});
+ Environments.resetMathMode();
+ };
+ } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) {
+ nextGroupMathMode = undefined;
+ };
+
+ } else if (type === "$") {
+ var lookAhead = Tokens[i+1];
+ var nextIsDollar = lookAhead && lookAhead[1] === "$";
+ currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) {
+ Environments.push({command:"$$", token:token});
+ i = i + 1;
+ } else {
+ Environments.push({command:"$", token:token});
}
- } else if (type === "{") {
- 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;
};
-
-var CheckEnvironments = function (Environments, ErrorReporter) {
+var EnvHandler = function (ErrorReporter) {
var ErrorTo = ErrorReporter.EnvErrorTo;
var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
var ErrorFrom = ErrorReporter.EnvErrorFrom;
+ var envs = [];
+
var state = [];
var documentClosed = null;
var inVerbatim = false;
var verbatimRanges = [];
- 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 invalidEnvs = [];
+
+ this._end = function (thisEnv) {
+ do {
var lastEnv = state.pop();
+ var retry = false;
+ var i;
- 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
- }
- };
-
- 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;
};
- continue;
+ return;
} 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 + "}");
- }
+ if (documentClosed) {
+ ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+ } else {
+ ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
}
- } else if (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;
- }
- }
- 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;
}
- }
- while (state.length > 0) {
- thisEnv = state.pop();
- if (thisEnv.command === "{") {
- ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
- } else if (thisEnv.command === "begin") {
- ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
+ };
+
+ var indexOfClosingEnvInArray = function (envs, thisEnv) {
+ for (var i = 0, n = envs.length; i < n ; i++) {
+ if (closedBy(envs[i], thisEnv)) {
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ var envPrecedence = function (env) {
+ var openScore = {
+ "{" : 1,
+ "left" : 2,
+ "$" : 3,
+ "$$" : 4,
+ "begin": 4
};
- }
- var 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;
+ var closeScore = {
+ "}" : 1,
+ "right" : 2,
+ "$" : 3,
+ "$$" : 5,
+ "end": 4
+ };
+ if (env.command) {
+ return openScore[env.command] || closeScore[env.command];
+ } else {
+ return 0;
+ }
+ };
+
+ var getName = function(env) {
+ var description = {
+ "{" : "open group {",
+ "}" : "close group }",
+ "[" : "open display math \\[",
+ "]" : "close display math \\]",
+ "(" : "open inline math \\(",
+ ")" : "close inline math \\)",
+ "$" : "$",
+ "$$" : "$$",
+ "left" : "\\left",
+ "right" : "\\right"
+ };
+ if (env.command === "begin" || env.command === "end") {
+ return "\\" + env.command + "{" + env.name + "}";
+ } else if (env.command in description) {
+ return description[env.command];
+ } else {
+ return env.command;
+ }
+ };
+
+ var EXTRA_CLOSE = 1;
+ var UNCLOSED_GROUP = 2;
+ var UNCLOSED_ENV = 3;
+
+ var reportError = function(lastEnv, thisEnv) {
+ if (!lastEnv) { // unexpected close, nothing was open!
+ if (documentClosed) {
+ ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
+ } else {
+ ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
+ };
+ return EXTRA_CLOSE;
+ } else if (lastEnv.command === "{" && thisEnv.command === "end") {
+ ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
+ {suppressIfEditing:true, errorAtStart: true, type:"warning"});
+ return UNCLOSED_GROUP;
+ } else {
+ var pLast = envPrecedence(lastEnv);
+ var pThis = envPrecedence(thisEnv);
+ if (pThis > pLast) {
+ ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
+ {suppressIfEditing:true, errorAtStart: true});
+ } else {
+ ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv));
+ }
+ return UNCLOSED_ENV;
+ };
+ };
+
+ this._beginMathMode = function (thisEnv) {
+ var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
+ if (currentMathMode) {
+ ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
+ {suppressIfEditing:true, errorAtStart: true, mathMode:true});
+ };
+ thisEnv.mathMode = thisEnv;
+ state.push(thisEnv);
+ };
+
+ this._toggleMathMode = function (thisEnv) {
+ var lastEnv = state.pop();
+ if (closedBy(lastEnv, thisEnv)) {
+ return;
+ } else {
+ if (lastEnv) {state.push(lastEnv);}
+ if (lastEnv && lastEnv.mathMode) {
+ this._end(thisEnv);
+ } else {
+ thisEnv.mathMode = thisEnv;
+ state.push(thisEnv);
+ }
+ };
+ };
+
+ this.getMathMode = function () {
+ var n = state.length;
+ if (n > 0) {
+ return state[n-1].mathMode;
+ } else {
+ return null;
+ }
+ };
+
+ this.insideGroup = function () {
+ var n = state.length;
+ if (n > 0) {
+ return (state[n-1].command === "{");
+ } else {
+ return null;
+ }
+ };
+
+ var resetMathMode = function () {
+ var n = state.length;
+ if (n > 0) {
+ var lastMathMode = state[n-1].mathMode;
+ do {
+ var lastEnv = state.pop();
+ } while (lastEnv && lastEnv !== lastMathMode);
+ } else {
+ return;
+ }
+ };
+
+ this.resetMathMode = resetMathMode;
+
+ var getNewMathMode = function (currentMathMode, thisEnv) {
+ var newMathMode = null;
+
+ if (thisEnv.command === "{") {
+ if (thisEnv.mathMode !== null) {
+ newMathMode = thisEnv.mathMode;
+ } else {
+ newMathMode = currentMathMode;
+ }
+ } else if (thisEnv.command === "left") {
+ if (currentMathMode === null) {
+ ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true});
+ };
+ newMathMode = currentMathMode;
+ } else if (thisEnv.command === "begin") {
+ var name = thisEnv.name;
+ if (name) {
+ if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
+ if (currentMathMode) {
+ ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
+ {suppressIfEditing:true, errorAtStart: true, mathMode: true});
+ resetMathMode();
+ };
+ newMathMode = null;
+ } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) {
+ if (currentMathMode === null) {
+ ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true});
+ };
+ newMathMode = currentMathMode;
+ } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
+ if (currentMathMode) {
+ ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
+ {suppressIfEditing:true, errorAtStart: true, mathMode: true});
+ resetMathMode();
+ };
+ newMathMode = thisEnv;
+ } else {
+ newMathMode = undefined; // undefined means we don't know if we are in math mode or not
+ }
+ }
+ };
+ return newMathMode;
+ };
+
+ this.checkAndUpdateState = function (thisEnv) {
+ if (inVerbatim) {
+ if (thisEnv.command === "end") {
+ this._endVerbatim(thisEnv);
+ } else {
+ return; // ignore anything in verbatim environments
+ }
+ } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") {
+ if (thisEnv.verbatim) {inVerbatim = true;};
+ var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
+ var newMathMode = getNewMathMode(currentMathMode, thisEnv);
+ thisEnv.mathMode = newMathMode;
+ state.push(thisEnv);
+ } else if (thisEnv.command === "end") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "(" || thisEnv.command === "[") {
+ this._beginMathMode(thisEnv);
+ } else if (thisEnv.command === ")" || thisEnv.command === "]") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "}") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "right") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "$" || thisEnv.command === "$$") {
+ this._toggleMathMode(thisEnv);
+ }
+ };
+
+ this.close = function () {
+ while (state.length > 0) {
+ var thisEnv = state.pop();
+ if (thisEnv.command === "{") {
+ ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
+ } else {
+ ErrorFrom(thisEnv, "unclosed " + getName(thisEnv));
+ }
+ }
+ var vlen = verbatimRanges.length;
+ var len = ErrorReporter.tokenErrors.length;
+ if (vlen >0 && len > 0) {
+ for (var i = 0; i < len; i++) {
+ var tokenError = ErrorReporter.tokenErrors[i];
+ var startPos = tokenError.startPos;
+ var endPos = tokenError.endPos;
+ for (var j = 0; j < vlen; j++) {
+ if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
+ tokenError.ignore = true;
+ break;
+ }
}
}
}
- }
+ };
+ this.setEnvProps = function (env) {
+ var name = env.name ;
+ if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) {
+ env.verbatim = true;
+ }
+ };
};
var ErrorReporter = function (TokeniseResult) {
var text = TokeniseResult.text;
@@ -1922,18 +2346,41 @@ var ErrorReporter = function (TokeniseResult) {
var errors = [], tokenErrors = [];
this.errors = errors;
this.tokenErrors = tokenErrors;
+ this.filterMath = false;
this.getErrors = function () {
var returnedErrors = [];
for (var i = 0, len = tokenErrors.length; i < len; i++) {
if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
}
- 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 start_col = start - linePosition[line];
+ if (!end) { end = start + 1; } ;
var end_col = end - linePosition[line];
tokenErrors.push({row: line,
column: start_col,
@@ -1945,10 +2392,12 @@ var ErrorReporter = function (TokeniseResult) {
text:message,
startPos: start,
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 toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
if (!toEnd) { toEnd = toStart + 1;};
@@ -1965,7 +2414,8 @@ var ErrorReporter = function (TokeniseResult) {
text:message,
startPos: fromStart,
endPos: toEnd,
- suppressIfEditing:true});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
@@ -1986,7 +2436,8 @@ var ErrorReporter = function (TokeniseResult) {
end_col: end_col,
type: options.type ? options.type : "error",
text:message,
- suppressIfEditing:options.suppressIfEditing});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
this.EnvErrorTo = function (toEnv, message, options) {
@@ -2002,7 +2453,8 @@ var ErrorReporter = function (TokeniseResult) {
end_row: line,
end_col: end_col,
type: options.type ? options.type : "error",
- text:message};
+ text:message,
+ mathMode: options.mathMode};
errors.push(err);
};
@@ -2019,7 +2471,8 @@ var ErrorReporter = function (TokeniseResult) {
end_row: lineNumber,
end_col: end_col,
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 Reporter = new ErrorReporter(TokeniseResult);
var Environments = InterpretTokens(TokeniseResult, Reporter);
- CheckEnvironments(Environments, Reporter);
+ Environments.close();
return Reporter.getErrors();
};
diff --git a/services/web/public/js/ace-1.2.5/worker-latex_beta.js b/services/web/public/js/ace-1.2.5/worker-latex_beta.js
index b47d8f0a46..720c3e5009 100644
--- a/services/web/public/js/ace-1.2.5/worker-latex_beta.js
+++ b/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 Tokens = TokeniseResult.tokens;
@@ -1624,9 +1643,56 @@ var readOptionalParams = function(TokeniseResult, k) {
return k + 1; // got it
};
};
+ var count = 0;
+ var nextToken = Tokens[k+1];
+ var pos = nextToken[2];
+
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === "[") { count++; }
+ if (char === "]") { count--; }
+ if (count === 0 && char === "{") { return k - 1; }
+ if (count > 0 && (char === '\r' || char === '\n')) { return null; }
+ };
return null;
};
+var readOptionalGeneric = function(TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var params = Tokens[k+1];
+
+ if(params && params[1] === "Text") {
+ var paramNum = text.substring(params[2], params[3]);
+ if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) {
+ return k + 1; // got it
+ };
+ };
+ return null;
+};
+
+var readOptionalDef = function (TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var defToken = Tokens[k];
+ var pos = defToken[3];
+
+ var openBrace = "{";
+ var nextToken = Tokens[k+1];
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments
+ if (char === '\r' || char === '\n') { return null; }
+ };
+
+ return null;
+
+};
+
var readDefinition = function(TokeniseResult, k) {
var Tokens = TokeniseResult.tokens;
var text = TokeniseResult.text;
@@ -1726,10 +1792,27 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var Environments = new EnvHandler(ErrorReporter);
var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
+ var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes
+ var seenUserDefinedBeginEquation = false; // if we have seen macros like \beq
+ var seenUserDefinedEndEquation = false; // if we have seen macros like \eeq
for (var i = 0, len = Tokens.length; i < len; i++) {
var token = Tokens[i];
var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
+
+ if (type === "{") {
+ Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
+ nextGroupMathModeStack.push(nextGroupMathMode);
+ nextGroupMathMode = null;
+ continue;
+ } else if (type === "}") {
+ Environments.push({command:"}", token:token});
+ nextGroupMathMode = nextGroupMathModeStack.pop();
+ continue;
+ } else {
+ nextGroupMathMode = null;
+ };
+
if (type === "\\") {
if (seq === "begin" || seq === "end") {
var open = Tokens[i+1];
@@ -1778,15 +1861,31 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
} else {
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;};
newPos = readOptionalParams(TokeniseResult, i);
if (newPos === null) { /* do nothing */ } else {i = newPos;};
newPos = readDefinition(TokeniseResult, i);
if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (seq === "def") {
+ newPos = read1arg(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ newPos = readOptionalDef(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ } else if (seq === "let") {
+ newPos = readLetDefinition(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+
} else if (seq === "newcolumntype") {
newPos = read1name(TokeniseResult, i);
if (newPos === null) { continue; } else {i = newPos;};
@@ -1820,7 +1919,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
} else if (nextToken && nextToken[1] === "\\") {
char = "unknown";
}
- if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) {
+ if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) {
TokenError(token, "invalid bracket command");
} else {
i = i + 1;
@@ -1831,25 +1930,50 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
} else if (seq === "input") {
newPos = read1filename(TokeniseResult, i);
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;
+ } 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 && !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)
- 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();
};
+ } 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 === "$") {
var lookAhead = Tokens[i+1];
var nextIsDollar = lookAhead && lookAhead[1] === "$";
@@ -1864,12 +1988,15 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
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");
+ TokenError(token, type + " must be inside math mode", {mathMode:true});
};
- } else {
- nextGroupMathMode = null;
}
};
+
+ if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) {
+ ErrorReporter.filterMath = true;
+ };
+
return Environments;
};
@@ -1920,7 +2047,7 @@ var EnvHandler = function (ErrorReporter) {
if (documentClosed) {
ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
} else {
- ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
+ ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
}
} else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
invalidEnvs.splice(i, 1);
@@ -2054,7 +2181,7 @@ var EnvHandler = function (ErrorReporter) {
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});
+ {suppressIfEditing:true, errorAtStart: true, mathMode:true});
};
thisEnv.mathMode = thisEnv;
state.push(thisEnv);
@@ -2118,28 +2245,28 @@ var EnvHandler = function (ErrorReporter) {
}
} else if (thisEnv.command === "left") {
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;
} else if (thisEnv.command === "begin") {
var name = thisEnv.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) {
ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
- {suppressIfEditing:true, errorAtStart: true});
+ {suppressIfEditing:true, errorAtStart: true, mathMode: true});
resetMathMode();
};
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;
} 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});
+ {suppressIfEditing:true, errorAtStart: true, mathMode: true});
resetMathMode();
};
newMathMode = thisEnv;
@@ -2220,13 +2347,36 @@ var ErrorReporter = function (TokeniseResult) {
var errors = [], tokenErrors = [];
this.errors = errors;
this.tokenErrors = tokenErrors;
+ this.filterMath = false;
this.getErrors = function () {
var returnedErrors = [];
for (var i = 0, len = tokenErrors.length; i < len; i++) {
if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
}
- 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) {
@@ -2245,7 +2395,8 @@ var ErrorReporter = function (TokeniseResult) {
text:message,
startPos: start,
endPos: end,
- suppressIfEditing:options.suppressIfEditing});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
@@ -2266,7 +2417,8 @@ var ErrorReporter = function (TokeniseResult) {
text:message,
startPos: fromStart,
endPos: toEnd,
- suppressIfEditing:options.suppressIfEditing});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
@@ -2287,7 +2439,8 @@ var ErrorReporter = function (TokeniseResult) {
end_col: end_col,
type: options.type ? options.type : "error",
text:message,
- suppressIfEditing:options.suppressIfEditing});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
this.EnvErrorTo = function (toEnv, message, options) {
@@ -2303,7 +2456,8 @@ var ErrorReporter = function (TokeniseResult) {
end_row: line,
end_col: end_col,
type: options.type ? options.type : "error",
- text:message};
+ text:message,
+ mathMode: options.mathMode};
errors.push(err);
};
@@ -2320,7 +2474,8 @@ var ErrorReporter = function (TokeniseResult) {
end_row: lineNumber,
end_col: end_col,
type: options.type ? options.type : "error",
- text:message});
+ text:message,
+ mathMode: options.mathMode});
};
};
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
index 72265eac11..515b888911 100644
--- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -387,6 +387,10 @@ describe "AuthenticationController", ->
beforeEach ->
@req.headers = {}
@AuthenticationController.httpAuth = sinon.stub()
+ @_setRedirect = sinon.spy(@AuthenticationController, '_setRedirectInSession')
+
+ afterEach ->
+ @_setRedirect.restore()
describe "with white listed url", ->
beforeEach ->
@@ -431,6 +435,9 @@ describe "AuthenticationController", ->
@req.session = {}
@AuthenticationController.requireGlobalLogin @req, @res, @next
+ it 'should have called setRedirectInSession', ->
+ @_setRedirect.callCount.should.equal 1
+
it "should redirect to the /login page", ->
@res.redirectedTo.should.equal "/login"
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 28bf1ab6a2..cf398e69da 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -255,6 +255,37 @@ describe "CollaboratorsInviteController", ->
it 'should not have called inviteToProject', ->
@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", ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index ac94fcf10d..177c42d4ba 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -185,7 +185,7 @@ describe "CollaboratorsInviteHandler", ->
describe '_sendMessages', ->
beforeEach ->
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, null)
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, null)
@CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null)
@call = (callback) =>
@CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback
@@ -213,7 +213,7 @@ describe "CollaboratorsInviteHandler", ->
describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', ->
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) ->
@call (err, invite) =>
diff --git a/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee b/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
index 4f08dd6790..beba91fcb3 100644
--- a/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
+++ b/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
@@ -10,6 +10,9 @@ describe "EmailSender", ->
beforeEach ->
+ @RateLimiter =
+ addCount:sinon.stub()
+
@settings =
email:
transport: "ses"
@@ -21,11 +24,15 @@ describe "EmailSender", ->
@sesClient =
sendMail: sinon.stub()
+
@ses =
createTransport: => @sesClient
+
+
@sender = SandboxedModule.require modulePath, requires:
'nodemailer': @ses
"settings-sharelatex":@settings
+ '../../infrastructure/RateLimiter':@RateLimiter
"logger-sharelatex":
log:->
warn:->
@@ -84,6 +91,29 @@ describe "EmailSender", ->
args.replyTo.should.equal @opts.replyTo
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', () ->
beforeEach ->