diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee
index a27608343a..ce41e3b96c 100644
--- a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee
+++ b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee
@@ -25,8 +25,15 @@ module.exports =
announcementIndex = _.findIndex announcements, (announcement)->
announcement.id == lastSeenBlogId
- if announcementIndex != -1
- announcements = announcements.slice(0, announcementIndex)
+ announcements = _.map announcements, (announcement, index)->
+ if announcementIndex == -1
+ read = false
+ else if index >= announcementIndex
+ read = true
+ else
+ read = false
+ announcement.read = read
+ return announcement
logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements"
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/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index b88ded33b2..9d9f4d2a5e 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -4,6 +4,7 @@ UserGetter = require "../User/UserGetter"
CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
+Settings = require('settings-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
@@ -21,6 +22,16 @@ module.exports = CollaboratorsInviteController =
return next(err)
res.json({invites: invites})
+ _checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
+ if Settings.restrictInvitesToExistingAccounts == true
+ logger.log {email}, "checking if user exists with this email"
+ UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
+ return callback(err) if err?
+ userExists = user? and user?._id?
+ callback(null, userExists)
+ else
+ callback(null, true)
+
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
@@ -37,13 +48,20 @@ module.exports = CollaboratorsInviteController =
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
- CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
+ CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
if err?
- logger.err {projectId, email, sendingUserId}, "error creating project invite"
+ logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
return next(err)
- logger.log {projectId, email, sendingUserId}, "invite created"
- EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
- return res.json {invite: invite}
+ if !shouldAllowInvite
+ logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
+ return res.json {invite: null, error: 'cannot_invite_non_user'}
+ CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
+ if err?
+ logger.err {projectId, email, sendingUserId}, "error creating project invite"
+ return next(err)
+ logger.log {projectId, email, sendingUserId}, "invite created"
+ EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
+ return res.json {invite: invite}
revokeInvite: (req, res, next) ->
projectId = req.params.Project_id
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/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/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index 9713a1b1e2..3e6beaa0fb 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -197,7 +197,7 @@ module.exports = ProjectController =
user_id = null
project_id = req.params.Project_id
- logger.log project_id:project_id, "loading editor"
+ logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor"
async.parallel {
project: (cb)->
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index e469df9422..867583468b 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -244,6 +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
+ 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/general/closed.jade b/services/web/app/views/general/closed.jade
index 9f21372c81..4a27e84681 100644
--- a/services/web/app/views/general/closed.jade
+++ b/services/web/app/views/general/closed.jade
@@ -11,4 +11,4 @@ block content
| Sorry, ShareLaTeX is briefly down for maintenance.
| We should be back within minutes, but if not, or you have
| an urgent request, please contact us at
- | support@sharelatex.com
+ | #{settings.adminEmail}
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 fd13ccb240..62de414064 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -137,10 +137,15 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
p.small(ng-show="startedFreeTrial")
| #{translate("refresh_page_after_starting_free_trial")}.
- .modal-footer
+ .modal-footer.modal-footer-share
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
- span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
+ span.text-danger.error(ng-show="state.error")
+ span(ng-switch="state.errorReason")
+ span(ng-switch-when="cannot_invite_non_user")
+ | #{translate("cannot_invite_non_user")}
+ span(ng-switch-default)
+ | #{translate("generic_something_went_wrong")}
button.btn.btn-default(
ng-click="done()"
) #{translate("close")}
diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade
index 6f1d87424a..d9fbf0e6b1 100644
--- a/services/web/app/views/project/list.jade
+++ b/services/web/app/views/project/list.jade
@@ -19,12 +19,46 @@ block content
}
};
- .content.content-alt(ng-controller="ProjectPageController")
+ .content.content-alt.project-list-page(ng-controller="ProjectPageController")
.container
-
- //- div(ng-controller="AnnouncementsController", ng-cloak)
- //- .alert.alert-success(ng-show="dataRecived")
- //- a(href, ng-click="openLink()") {{title}} and {{totalAnnouncements}} others
+ .announcements(
+ ng-controller="AnnouncementsController"
+ ng-class="{ 'announcements-open': ui.isOpen }"
+ ng-cloak
+ )
+ .announcements-backdrop(
+ ng-if="ui.isOpen"
+ ng-click="toggleAnnouncementsUI();"
+ )
+ a.announcements-btn(
+ href
+ ng-if="announcements.length"
+ ng-click="toggleAnnouncementsUI();"
+ ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.newItems }"
+ )
+ span.announcements-badge(ng-if="ui.newItems") {{ ui.newItems }}
+ .announcements-body(
+ ng-if="ui.isOpen"
+ )
+ .announcements-scroller
+ .announcement(
+ ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id"
+ )
+ h2.announcement-header {{ announcement.title }}
+ p.announcement-description(ng-bind-html="announcement.excerpt")
+ .announcement-meta
+ p.announcement-date {{ announcement.date | date:"longDate" }}
+ a.announcement-link(
+ ng-href="{{ announcement.url }}"
+ target="_blank"
+ ) Read more
+ div.text-center(
+ ng-if="ui.newItems > 0 && ui.newItems < announcements.length"
+ )
+ a.btn.btn-default.btn-sm(
+ href
+ ng-click="showAll();"
+ ) Show all
.row(ng-cloak)
span(ng-if="projects.length > 0")
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index e3b842a0d2..44e4a75867 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -276,6 +276,10 @@ module.exports = settings =
# Cookie max age (in milliseconds). Set to false for a browser session.
cookieSessionLength: 5 * 24 * 60 * 60 * 1000 # 5 days
+ # When true, only allow invites to be sent to email addresses that
+ # already have user accounts
+ restrictInvitesToExistingAccounts: false
+
# Should we allow access to any page without logging in? This includes
# public projects, /learn, /templates, about pages, etc.
allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
@@ -331,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/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
index 88ce71fcd2..8a3b00a61e 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -269,6 +269,10 @@ define [
catch
mode = "ace/mode/plain_text"
+ # Give beta users the next release of the syntax checker
+ if mode is "ace/mode/latex" and window.user?.betaProgram
+ mode = "ace/mode/latex_beta"
+
# create our new session
session = new EditSession(lines, mode)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 2d27f96970..6ab15de766 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -8,6 +8,7 @@ define [
}
$scope.state = {
error: null
+ errorReason: null
inflight: false
startedFreeTrial: false
invites: []
@@ -69,7 +70,8 @@ define [
members = $scope.inputs.contacts
$scope.inputs.contacts = []
- $scope.state.error = null
+ $scope.state.error = false
+ $scope.state.errorReason = null
$scope.state.inflight = true
if !$scope.project.invites?
@@ -101,17 +103,22 @@ define [
request
.success (data) ->
- if data.invite
- invite = data.invite
- $scope.project.invites.push invite
+ if data.error
+ $scope.state.error = true
+ $scope.state.errorReason = "#{data.error}"
+ $scope.state.inflight = false
else
- if data.users?
- users = data.users
- else if data.user?
- users = [data.user]
+ if data.invite
+ invite = data.invite
+ $scope.project.invites.push invite
else
- users = []
- $scope.project.members.push users...
+ if data.users?
+ users = data.users
+ else if data.user?
+ users = [data.user]
+ else
+ users = []
+ $scope.project.members.push users...
setTimeout () ->
# Give $scope a chance to update $scope.canAddCollaborators
@@ -121,6 +128,7 @@ define [
.error () ->
$scope.state.inflight = false
$scope.state.error = true
+ $scope.state.errorReason = null
$timeout addMembers, 50 # Give email list a chance to update
diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee
index 471870f280..5ad6f37d34 100644
--- a/services/web/public/coffee/main.coffee
+++ b/services/web/public/coffee/main.coffee
@@ -33,4 +33,18 @@ define [
"filters/formatDate"
"__MAIN_CLIENTSIDE_INCLUDES__"
], () ->
- angular.bootstrap(document.body, ["SharelatexApp"])
+ angular.module('SharelatexApp').config(
+ ($locationProvider) ->
+ try
+ $locationProvider.html5Mode({
+ enabled: false,
+ requireBase: false,
+ rewriteLinks: false
+ })
+ catch e
+ console.error "Error while trying to fix '#' links: ", e
+ )
+ angular.bootstrap(
+ document.body,
+ ["SharelatexApp"]
+ )
diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee
index b0960bcc09..cccebf56ad 100644
--- a/services/web/public/coffee/main/announcements.coffee
+++ b/services/web/public/coffee/main/announcements.coffee
@@ -1,20 +1,29 @@
define [
"base"
], (App) ->
- App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window) ->
+ App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window, _) ->
+ $scope.announcements = []
+ $scope.ui =
+ isOpen: false
+ newItems: 0
+
+ refreshAnnouncements = ->
+ $http.get("/announcements").success (announcements) ->
+ $scope.announcements = announcements
+ $scope.ui.newItems = _.filter(announcements, (announcement) -> !announcement.read).length
+
+ markAnnouncementsAsRead = ->
+ event_tracking.sendMB "announcement-alert-dismissed", { blogPostId: $scope.announcements[0].id }
- $scope.dataRecived = false
- announcement = null
- $http.get("/announcements").success (announcements) ->
- if announcements?[0]?
- announcement = announcements[0]
- $scope.title = announcement.title
- $scope.totalAnnouncements = announcements.length
- $scope.dataRecived = true
+ refreshAnnouncements()
- dismissannouncement = ->
- event_tracking.sendMB "announcement-alert-dismissed", {blogPostId:announcement.id}
+ $scope.toggleAnnouncementsUI = ->
+ $scope.ui.isOpen = !$scope.ui.isOpen
+
+ if !$scope.ui.isOpen and $scope.ui.newItems
+ $scope.ui.newItems = 0
+ markAnnouncementsAsRead()
+
+ $scope.showAll = ->
+ $scope.ui.newItems = 0
- $scope.openLink = ->
- dismissannouncement()
- $window.location.href = announcement.url
diff --git a/services/web/public/js/ace-1.2.5/mode-latex_beta.js b/services/web/public/js/ace-1.2.5/mode-latex_beta.js
new file mode 100644
index 0000000000..1a98491951
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/mode-latex_beta.js
@@ -0,0 +1,378 @@
+ace.define("ace/mode/latex_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
+"use strict";
+
+var oop = require("../lib/oop");
+var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
+
+var LatexHighlightRules = function() {
+
+ this.$rules = {
+ "start" : [{
+ token : "comment",
+ regex : "%.*$"
+ }, {
+ token : ["keyword", "lparen", "variable.parameter", "rparen", "lparen", "storage.type", "rparen"],
+ regex : "(\\\\(?:documentclass|usepackage|input))(?:(\\[)([^\\]]*)(\\]))?({)([^}]*)(})"
+ }, {
+ token : ["keyword","lparen", "variable.parameter", "rparen"],
+ regex : "(\\\\(?:label|v?ref|cite(?:[^{]*)))(?:({)([^}]*)(}))?"
+ }, {
+ token : ["storage.type", "lparen", "variable.parameter", "rparen"],
+ regex : "(\\\\(?:begin|end))({)(\\w*)(})"
+ }, {
+ token : "storage.type",
+ regex : "\\\\[a-zA-Z]+"
+ }, {
+ token : "lparen",
+ regex : "[[({]"
+ }, {
+ token : "rparen",
+ regex : "[\\])}]"
+ }, {
+ token : "constant.character.escape",
+ regex : "\\\\[^a-zA-Z]?"
+ }, {
+ token : "string",
+ regex : "\\${1,2}",
+ next : "equation"
+ }],
+ "equation" : [{
+ token : "comment",
+ regex : "%.*$"
+ }, {
+ token : "string",
+ regex : "\\${1,2}",
+ next : "start"
+ }, {
+ token : "constant.character.escape",
+ regex : "\\\\(?:[^a-zA-Z]|[a-zA-Z]+)"
+ }, {
+ token : "error",
+ regex : "^\\s*$",
+ next : "start"
+ }, {
+ defaultToken : "string"
+ }]
+
+ };
+};
+oop.inherits(LatexHighlightRules, TextHighlightRules);
+
+exports.LatexHighlightRules = LatexHighlightRules;
+
+});
+
+ace.define("ace/mode/folding/latex",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode","ace/range","ace/token_iterator"], function(require, exports, module) {
+"use strict";
+
+var oop = require("../../lib/oop");
+var BaseFoldMode = require("./fold_mode").FoldMode;
+var Range = require("../../range").Range;
+var TokenIterator = require("../../token_iterator").TokenIterator;
+
+var FoldMode = exports.FoldMode = function() {};
+
+oop.inherits(FoldMode, BaseFoldMode);
+
+(function() {
+
+ this.foldingStartMarker = /^\s*\\(begin)|(section|subsection|paragraph)\b|{\s*$/;
+ this.foldingStopMarker = /^\s*\\(end)\b|^\s*}/;
+
+ this.getFoldWidgetRange = function(session, foldStyle, row) {
+ var line = session.doc.getLine(row);
+ var match = this.foldingStartMarker.exec(line);
+ if (match) {
+ if (match[1])
+ return this.latexBlock(session, row, match[0].length - 1);
+ if (match[2])
+ return this.latexSection(session, row, match[0].length - 1);
+
+ return this.openingBracketBlock(session, "{", row, match.index);
+ }
+
+ var match = this.foldingStopMarker.exec(line);
+ if (match) {
+ if (match[1])
+ return this.latexBlock(session, row, match[0].length - 1);
+
+ return this.closingBracketBlock(session, "}", row, match.index + match[0].length);
+ }
+ };
+
+ this.latexBlock = function(session, row, column) {
+ var keywords = {
+ "\\begin": 1,
+ "\\end": -1
+ };
+
+ var stream = new TokenIterator(session, row, column);
+ var token = stream.getCurrentToken();
+ if (!token || !(token.type == "storage.type" || token.type == "constant.character.escape"))
+ return;
+
+ var val = token.value;
+ var dir = keywords[val];
+
+ var getType = function() {
+ var token = stream.stepForward();
+ var type = token.type == "lparen" ?stream.stepForward().value : "";
+ if (dir === -1) {
+ stream.stepBackward();
+ if (type)
+ stream.stepBackward();
+ }
+ return type;
+ };
+ var stack = [getType()];
+ var startColumn = dir === -1 ? stream.getCurrentTokenColumn() : session.getLine(row).length;
+ var startRow = row;
+
+ stream.step = dir === -1 ? stream.stepBackward : stream.stepForward;
+ while(token = stream.step()) {
+ if (!token || !(token.type == "storage.type" || token.type == "constant.character.escape"))
+ continue;
+ var level = keywords[token.value];
+ if (!level)
+ continue;
+ var type = getType();
+ if (level === dir)
+ stack.unshift(type);
+ else if (stack.shift() !== type || !stack.length)
+ break;
+ }
+
+ if (stack.length)
+ return;
+
+ var row = stream.getCurrentTokenRow();
+ if (dir === -1)
+ return new Range(row, session.getLine(row).length, startRow, startColumn);
+ stream.stepBackward();
+ return new Range(startRow, startColumn, row, stream.getCurrentTokenColumn());
+ };
+
+ this.latexSection = function(session, row, column) {
+ var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph"];
+
+ var stream = new TokenIterator(session, row, column);
+ var token = stream.getCurrentToken();
+ if (!token || token.type != "storage.type")
+ return;
+
+ var startLevel = keywords.indexOf(token.value);
+ var stackDepth = 0
+ var endRow = row;
+
+ while(token = stream.stepForward()) {
+ if (token.type !== "storage.type")
+ continue;
+ var level = keywords.indexOf(token.value);
+
+ if (level >= 2) {
+ if (!stackDepth)
+ endRow = stream.getCurrentTokenRow() - 1;
+ stackDepth += level == 2 ? 1 : - 1;
+ if (stackDepth < 0)
+ break
+ } else if (level >= startLevel)
+ break;
+ }
+
+ if (!stackDepth)
+ endRow = stream.getCurrentTokenRow() - 1;
+
+ while (endRow > row && !/\S/.test(session.getLine(endRow)))
+ endRow--;
+
+ return new Range(
+ row, session.getLine(row).length,
+ endRow, session.getLine(endRow).length
+ );
+ };
+
+}).call(FoldMode.prototype);
+
+});
+
+ace.define("ace/mode/latex_beta",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/latex_highlight_rules","ace/mode/folding/latex","ace/range","ace/worker/worker_client"], function(require, exports, module) {
+"use strict";
+
+var oop = require("../lib/oop");
+var TextMode = require("./text").Mode;
+var LatexHighlightRules = require("./latex_highlight_rules").LatexHighlightRules;
+var LatexFoldMode = require("./folding/latex").FoldMode;
+var Range = require("../range").Range;
+var WorkerClient = require("ace/worker/worker_client").WorkerClient;
+
+var createLatexWorker = function (session) {
+ var doc = session.getDocument();
+ var selection = session.getSelection();
+ var cursorAnchor = selection.lead;
+
+ var savedRange = {};
+ var suppressions = [];
+ var hints = [];
+ var changeHandler = null;
+ var docChangePending = false;
+ var firstPass = true;
+
+ var worker = new WorkerClient(["ace"], "ace/mode/latex_beta_worker", "LatexWorker");
+ worker.attachToDocument(doc);
+ var docChangeHandler = doc.on("change", function () {
+ docChangePending = true;
+ if(changeHandler) {
+ clearTimeout(changeHandler);
+ changeHandler = null;
+ }
+ });
+
+ var cursorHandler = selection.on("changeCursor", function () {
+ if (docChangePending) { return; } ;
+ changeHandler = setTimeout(function () {
+ updateMarkers({cursorMoveOnly:true});
+ suppressions = [];
+ changeHandler = null;
+ }, 100);
+ });
+
+ var updateMarkers = function (options) {
+ if (!options) { options = {};};
+ var cursorMoveOnly = options.cursorMoveOnly;
+ var annotations = [];
+ var newRange = {};
+ var cursor = selection.getCursor();
+ var maxRow = session.getLength() - 1;
+ var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0;
+ var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol);
+
+ suppressions = [];
+
+ for (var i = 0, len = hints.length; i 0) {
+ var originalAnnotations = session.getAnnotations();
+ session.setAnnotations(originalAnnotations.concat(annotations));
+ };
+ firstPass = false;
+ } else {
+ session.setAnnotations(annotations);
+ }
+ };
+
+ };
+ worker.on("lint", function(results) {
+ if(docChangePending) { docChangePending = false; };
+ hints = results.data;
+ if (hints.length > 100) {
+ hints = hints.slice(0, 100); // limit to 100 errors
+ };
+ updateMarkers();
+ });
+ worker.on("terminate", function() {
+ if(changeHandler) {
+ clearTimeout(changeHandler);
+ changeHandler = null;
+ }
+ doc.off("change", docChangeHandler);
+ selection.off("changeCursor", cursorHandler);
+ for (var key in savedRange) {
+ var range = savedRange[key];
+ if (range.start !== cursorAnchor) { range.start.detach(); }
+ if (range.end !== cursorAnchor) { range.end.detach(); }
+ session.removeMarker(range.id);
+ }
+ savedRange = {};
+ hints = [];
+ suppressions = [];
+ session.clearAnnotations();
+ });
+
+ return worker;
+};
+
+var Mode = function() {
+ this.HighlightRules = LatexHighlightRules;
+ this.foldingRules = new LatexFoldMode();
+ this.createWorker = createLatexWorker;
+};
+oop.inherits(Mode, TextMode);
+
+(function() {
+ this.type = "text";
+
+ this.lineCommentStart = "%";
+
+ this.$id = "ace/mode/latex_beta";
+}).call(Mode.prototype);
+
+exports.Mode = Mode;
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/latex_beta.js b/services/web/public/js/ace-1.2.5/snippets/latex_beta.js
new file mode 100644
index 0000000000..209a682be8
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/latex_beta.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/latex_beta",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "latex";
+
+});
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
new file mode 100644
index 0000000000..720c3e5009
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/worker-latex_beta.js
@@ -0,0 +1,3207 @@
+"no use strict";
+;(function(window) {
+if (typeof window.window != "undefined" && window.document)
+ return;
+if (window.require && window.define)
+ return;
+
+if (!window.console) {
+ window.console = function() {
+ var msgs = Array.prototype.slice.call(arguments, 0);
+ postMessage({type: "log", data: msgs});
+ };
+ window.console.error =
+ window.console.warn =
+ window.console.log =
+ window.console.trace = window.console;
+}
+window.window = window;
+window.ace = window;
+
+window.onerror = function(message, file, line, col, err) {
+ postMessage({type: "error", data: {
+ message: message,
+ data: err.data,
+ file: file,
+ line: line,
+ col: col,
+ stack: err.stack
+ }});
+};
+
+window.normalizeModule = function(parentId, moduleName) {
+ // normalize plugin requires
+ if (moduleName.indexOf("!") !== -1) {
+ var chunks = moduleName.split("!");
+ return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]);
+ }
+ // normalize relative requires
+ if (moduleName.charAt(0) == ".") {
+ var base = parentId.split("/").slice(0, -1).join("/");
+ moduleName = (base ? base + "/" : "") + moduleName;
+
+ while (moduleName.indexOf(".") !== -1 && previous != moduleName) {
+ var previous = moduleName;
+ moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "");
+ }
+ }
+
+ return moduleName;
+};
+
+window.require = function require(parentId, id) {
+ if (!id) {
+ id = parentId;
+ parentId = null;
+ }
+ if (!id.charAt)
+ throw new Error("worker.js require() accepts only (parentId, id) as arguments");
+
+ id = window.normalizeModule(parentId, id);
+
+ var module = window.require.modules[id];
+ if (module) {
+ if (!module.initialized) {
+ module.initialized = true;
+ module.exports = module.factory().exports;
+ }
+ return module.exports;
+ }
+
+ if (!window.require.tlns)
+ return console.log("unable to load " + id);
+
+ var path = resolveModuleId(id, window.require.tlns);
+ if (path.slice(-3) != ".js") path += ".js";
+
+ window.require.id = id;
+ window.require.modules[id] = {}; // prevent infinite loop on broken modules
+ importScripts(path);
+ return window.require(parentId, id);
+};
+function resolveModuleId(id, paths) {
+ var testPath = id, tail = "";
+ while (testPath) {
+ var alias = paths[testPath];
+ if (typeof alias == "string") {
+ return alias + tail;
+ } else if (alias) {
+ return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name);
+ } else if (alias === false) {
+ return "";
+ }
+ var i = testPath.lastIndexOf("/");
+ if (i === -1) break;
+ tail = testPath.substr(i) + tail;
+ testPath = testPath.slice(0, i);
+ }
+ return id;
+}
+window.require.modules = {};
+window.require.tlns = {};
+
+window.define = function(id, deps, factory) {
+ if (arguments.length == 2) {
+ factory = deps;
+ if (typeof id != "string") {
+ deps = id;
+ id = window.require.id;
+ }
+ } else if (arguments.length == 1) {
+ factory = id;
+ deps = [];
+ id = window.require.id;
+ }
+
+ if (typeof factory != "function") {
+ window.require.modules[id] = {
+ exports: factory,
+ initialized: true
+ };
+ return;
+ }
+
+ if (!deps.length)
+ // If there is no dependencies, we inject "require", "exports" and
+ // "module" as dependencies, to provide CommonJS compatibility.
+ deps = ["require", "exports", "module"];
+
+ var req = function(childId) {
+ return window.require(id, childId);
+ };
+
+ window.require.modules[id] = {
+ exports: {},
+ factory: function() {
+ var module = this;
+ var returnExports = factory.apply(this, deps.map(function(dep) {
+ switch (dep) {
+ // Because "require", "exports" and "module" aren't actual
+ // dependencies, we must handle them seperately.
+ case "require": return req;
+ case "exports": return module.exports;
+ case "module": return module;
+ // But for all other dependencies, we can just go ahead and
+ // require them.
+ default: return req(dep);
+ }
+ }));
+ if (returnExports)
+ module.exports = returnExports;
+ return module;
+ }
+ };
+};
+window.define.amd = {};
+require.tlns = {};
+window.initBaseUrls = function initBaseUrls(topLevelNamespaces) {
+ for (var i in topLevelNamespaces)
+ require.tlns[i] = topLevelNamespaces[i];
+};
+
+window.initSender = function initSender() {
+
+ var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter;
+ var oop = window.require("ace/lib/oop");
+
+ var Sender = function() {};
+
+ (function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.callback = function(data, callbackId) {
+ postMessage({
+ type: "call",
+ id: callbackId,
+ data: data
+ });
+ };
+
+ this.emit = function(name, data) {
+ postMessage({
+ type: "event",
+ name: name,
+ data: data
+ });
+ };
+
+ }).call(Sender.prototype);
+
+ return new Sender();
+};
+
+var main = window.main = null;
+var sender = window.sender = null;
+
+window.onmessage = function(e) {
+ var msg = e.data;
+ if (msg.event && sender) {
+ sender._signal(msg.event, msg.data);
+ }
+ else if (msg.command) {
+ if (main[msg.command])
+ main[msg.command].apply(main, msg.args);
+ else if (window[msg.command])
+ window[msg.command].apply(window, msg.args);
+ else
+ throw new Error("Unknown command:" + msg.command);
+ }
+ else if (msg.init) {
+ window.initBaseUrls(msg.tlns);
+ require("ace/lib/es5-shim");
+ sender = window.sender = window.initSender();
+ var clazz = require(msg.module)[msg.classname];
+ main = window.main = new clazz(sender);
+ }
+};
+})(this);
+
+ace.define("ace/lib/oop",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.inherits = function(ctor, superCtor) {
+ ctor.super_ = superCtor;
+ ctor.prototype = Object.create(superCtor.prototype, {
+ constructor: {
+ value: ctor,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+};
+
+exports.mixin = function(obj, mixin) {
+ for (var key in mixin) {
+ obj[key] = mixin[key];
+ }
+ return obj;
+};
+
+exports.implement = function(proto, mixin) {
+ exports.mixin(proto, mixin);
+};
+
+});
+
+ace.define("ace/range",["require","exports","module"], function(require, exports, module) {
+"use strict";
+var comparePoints = function(p1, p2) {
+ return p1.row - p2.row || p1.column - p2.column;
+};
+var Range = function(startRow, startColumn, endRow, endColumn) {
+ this.start = {
+ row: startRow,
+ column: startColumn
+ };
+
+ this.end = {
+ row: endRow,
+ column: endColumn
+ };
+};
+
+(function() {
+ this.isEqual = function(range) {
+ return this.start.row === range.start.row &&
+ this.end.row === range.end.row &&
+ this.start.column === range.start.column &&
+ this.end.column === range.end.column;
+ };
+ this.toString = function() {
+ return ("Range: [" + this.start.row + "/" + this.start.column +
+ "] -> [" + this.end.row + "/" + this.end.column + "]");
+ };
+
+ this.contains = function(row, column) {
+ return this.compare(row, column) == 0;
+ };
+ this.compareRange = function(range) {
+ var cmp,
+ end = range.end,
+ start = range.start;
+
+ cmp = this.compare(end.row, end.column);
+ if (cmp == 1) {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == 1) {
+ return 2;
+ } else if (cmp == 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (cmp == -1) {
+ return -2;
+ } else {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == -1) {
+ return -1;
+ } else if (cmp == 1) {
+ return 42;
+ } else {
+ return 0;
+ }
+ }
+ };
+ this.comparePoint = function(p) {
+ return this.compare(p.row, p.column);
+ };
+ this.containsRange = function(range) {
+ return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0;
+ };
+ this.intersects = function(range) {
+ var cmp = this.compareRange(range);
+ return (cmp == -1 || cmp == 0 || cmp == 1);
+ };
+ this.isEnd = function(row, column) {
+ return this.end.row == row && this.end.column == column;
+ };
+ this.isStart = function(row, column) {
+ return this.start.row == row && this.start.column == column;
+ };
+ this.setStart = function(row, column) {
+ if (typeof row == "object") {
+ this.start.column = row.column;
+ this.start.row = row.row;
+ } else {
+ this.start.row = row;
+ this.start.column = column;
+ }
+ };
+ this.setEnd = function(row, column) {
+ if (typeof row == "object") {
+ this.end.column = row.column;
+ this.end.row = row.row;
+ } else {
+ this.end.row = row;
+ this.end.column = column;
+ }
+ };
+ this.inside = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column) || this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+ this.insideStart = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+ this.insideEnd = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+ this.compare = function(row, column) {
+ if (!this.isMultiLine()) {
+ if (row === this.start.row) {
+ return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0);
+ }
+ }
+
+ if (row < this.start.row)
+ return -1;
+
+ if (row > this.end.row)
+ return 1;
+
+ if (this.start.row === row)
+ return column >= this.start.column ? 0 : -1;
+
+ if (this.end.row === row)
+ return column <= this.end.column ? 0 : 1;
+
+ return 0;
+ };
+ this.compareStart = function(row, column) {
+ if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ };
+ this.compareEnd = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else {
+ return this.compare(row, column);
+ }
+ };
+ this.compareInside = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ };
+ this.clipRows = function(firstRow, lastRow) {
+ if (this.end.row > lastRow)
+ var end = {row: lastRow + 1, column: 0};
+ else if (this.end.row < firstRow)
+ var end = {row: firstRow, column: 0};
+
+ if (this.start.row > lastRow)
+ var start = {row: lastRow + 1, column: 0};
+ else if (this.start.row < firstRow)
+ var start = {row: firstRow, column: 0};
+
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+ this.extend = function(row, column) {
+ var cmp = this.compare(row, column);
+
+ if (cmp == 0)
+ return this;
+ else if (cmp == -1)
+ var start = {row: row, column: column};
+ else
+ var end = {row: row, column: column};
+
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+
+ this.isEmpty = function() {
+ return (this.start.row === this.end.row && this.start.column === this.end.column);
+ };
+ this.isMultiLine = function() {
+ return (this.start.row !== this.end.row);
+ };
+ this.clone = function() {
+ return Range.fromPoints(this.start, this.end);
+ };
+ this.collapseRows = function() {
+ if (this.end.column == 0)
+ return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0)
+ else
+ return new Range(this.start.row, 0, this.end.row, 0)
+ };
+ this.toScreenRange = function(session) {
+ var screenPosStart = session.documentToScreenPosition(this.start);
+ var screenPosEnd = session.documentToScreenPosition(this.end);
+
+ return new Range(
+ screenPosStart.row, screenPosStart.column,
+ screenPosEnd.row, screenPosEnd.column
+ );
+ };
+ this.moveBy = function(row, column) {
+ this.start.row += row;
+ this.start.column += column;
+ this.end.row += row;
+ this.end.column += column;
+ };
+
+}).call(Range.prototype);
+Range.fromPoints = function(start, end) {
+ return new Range(start.row, start.column, end.row, end.column);
+};
+Range.comparePoints = comparePoints;
+
+Range.comparePoints = function(p1, p2) {
+ return p1.row - p2.row || p1.column - p2.column;
+};
+
+
+exports.Range = Range;
+});
+
+ace.define("ace/apply_delta",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+function throwDeltaError(delta, errorText){
+ console.log("Invalid Delta:", delta);
+ throw "Invalid Delta: " + errorText;
+}
+
+function positionInDocument(docLines, position) {
+ return position.row >= 0 && position.row < docLines.length &&
+ position.column >= 0 && position.column <= docLines[position.row].length;
+}
+
+function validateDelta(docLines, delta) {
+ if (delta.action != "insert" && delta.action != "remove")
+ throwDeltaError(delta, "delta.action must be 'insert' or 'remove'");
+ if (!(delta.lines instanceof Array))
+ throwDeltaError(delta, "delta.lines must be an Array");
+ if (!delta.start || !delta.end)
+ throwDeltaError(delta, "delta.start/end must be an present");
+ var start = delta.start;
+ if (!positionInDocument(docLines, delta.start))
+ throwDeltaError(delta, "delta.start must be contained in document");
+ var end = delta.end;
+ if (delta.action == "remove" && !positionInDocument(docLines, end))
+ throwDeltaError(delta, "delta.end must contained in document for 'remove' actions");
+ var numRangeRows = end.row - start.row;
+ var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0));
+ if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars)
+ throwDeltaError(delta, "delta.range must match delta lines");
+}
+
+exports.applyDelta = function(docLines, delta, doNotValidate) {
+
+ var row = delta.start.row;
+ var startColumn = delta.start.column;
+ var line = docLines[row] || "";
+ switch (delta.action) {
+ case "insert":
+ var lines = delta.lines;
+ if (lines.length === 1) {
+ docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
+ } else {
+ var args = [row, 1].concat(delta.lines);
+ docLines.splice.apply(docLines, args);
+ docLines[row] = line.substring(0, startColumn) + docLines[row];
+ docLines[row + delta.lines.length - 1] += line.substring(startColumn);
+ }
+ break;
+ case "remove":
+ var endColumn = delta.end.column;
+ var endRow = delta.end.row;
+ if (row === endRow) {
+ docLines[row] = line.substring(0, startColumn) + line.substring(endColumn);
+ } else {
+ docLines.splice(
+ row, endRow - row + 1,
+ line.substring(0, startColumn) + docLines[endRow].substring(endColumn)
+ );
+ }
+ break;
+ }
+}
+});
+
+ace.define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+var EventEmitter = {};
+var stopPropagation = function() { this.propagationStopped = true; };
+var preventDefault = function() { this.defaultPrevented = true; };
+
+EventEmitter._emit =
+EventEmitter._dispatchEvent = function(eventName, e) {
+ this._eventRegistry || (this._eventRegistry = {});
+ this._defaultHandlers || (this._defaultHandlers = {});
+
+ var listeners = this._eventRegistry[eventName] || [];
+ var defaultHandler = this._defaultHandlers[eventName];
+ if (!listeners.length && !defaultHandler)
+ return;
+
+ if (typeof e != "object" || !e)
+ e = {};
+
+ if (!e.type)
+ e.type = eventName;
+ if (!e.stopPropagation)
+ e.stopPropagation = stopPropagation;
+ if (!e.preventDefault)
+ e.preventDefault = preventDefault;
+
+ listeners = listeners.slice();
+ for (var i=0; i this.row)
+ return;
+
+ var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight);
+ this.setPosition(point.row, point.column, true);
+ };
+
+ function $pointsInOrder(point1, point2, equalPointsInOrder) {
+ var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column;
+ return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter);
+ }
+
+ function $getTransformedPoint(delta, point, moveIfEqual) {
+ var deltaIsInsert = delta.action == "insert";
+ var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row);
+ var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column);
+ var deltaStart = delta.start;
+ var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range.
+ if ($pointsInOrder(point, deltaStart, moveIfEqual)) {
+ return {
+ row: point.row,
+ column: point.column
+ };
+ }
+ if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) {
+ return {
+ row: point.row + deltaRowShift,
+ column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0)
+ };
+ }
+
+ return {
+ row: deltaStart.row,
+ column: deltaStart.column
+ };
+ }
+ this.setPosition = function(row, column, noClip) {
+ var pos;
+ if (noClip) {
+ pos = {
+ row: row,
+ column: column
+ };
+ } else {
+ pos = this.$clipPositionToDocument(row, column);
+ }
+
+ if (this.row == pos.row && this.column == pos.column)
+ return;
+
+ var old = {
+ row: this.row,
+ column: this.column
+ };
+
+ this.row = pos.row;
+ this.column = pos.column;
+ this._signal("change", {
+ old: old,
+ value: pos
+ });
+ };
+ this.detach = function() {
+ this.document.removeEventListener("change", this.$onChange);
+ };
+ this.attach = function(doc) {
+ this.document = doc || this.document;
+ this.document.on("change", this.$onChange);
+ };
+ this.$clipPositionToDocument = function(row, column) {
+ var pos = {};
+
+ if (row >= this.document.getLength()) {
+ pos.row = Math.max(0, this.document.getLength() - 1);
+ pos.column = this.document.getLine(pos.row).length;
+ }
+ else if (row < 0) {
+ pos.row = 0;
+ pos.column = 0;
+ }
+ else {
+ pos.row = row;
+ pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column));
+ }
+
+ if (column < 0)
+ pos.column = 0;
+
+ return pos;
+ };
+
+}).call(Anchor.prototype);
+
+});
+
+ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) {
+"use strict";
+
+var oop = require("./lib/oop");
+var applyDelta = require("./apply_delta").applyDelta;
+var EventEmitter = require("./lib/event_emitter").EventEmitter;
+var Range = require("./range").Range;
+var Anchor = require("./anchor").Anchor;
+
+var Document = function(textOrLines) {
+ this.$lines = [""];
+ if (textOrLines.length === 0) {
+ this.$lines = [""];
+ } else if (Array.isArray(textOrLines)) {
+ this.insertMergedLines({row: 0, column: 0}, textOrLines);
+ } else {
+ this.insert({row: 0, column:0}, textOrLines);
+ }
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+ this.setValue = function(text) {
+ var len = this.getLength() - 1;
+ this.remove(new Range(0, 0, len, this.getLine(len).length));
+ this.insert({row: 0, column: 0}, text);
+ };
+ this.getValue = function() {
+ return this.getAllLines().join(this.getNewLineCharacter());
+ };
+ this.createAnchor = function(row, column) {
+ return new Anchor(this, row, column);
+ };
+ if ("aaa".split(/a/).length === 0) {
+ this.$split = function(text) {
+ return text.replace(/\r\n|\r/g, "\n").split("\n");
+ };
+ } else {
+ this.$split = function(text) {
+ return text.split(/\r\n|\r|\n/);
+ };
+ }
+
+
+ this.$detectNewLine = function(text) {
+ var match = text.match(/^.*?(\r\n|\r|\n)/m);
+ this.$autoNewLine = match ? match[1] : "\n";
+ this._signal("changeNewLineMode");
+ };
+ this.getNewLineCharacter = function() {
+ switch (this.$newLineMode) {
+ case "windows":
+ return "\r\n";
+ case "unix":
+ return "\n";
+ default:
+ return this.$autoNewLine || "\n";
+ }
+ };
+
+ this.$autoNewLine = "";
+ this.$newLineMode = "auto";
+ this.setNewLineMode = function(newLineMode) {
+ if (this.$newLineMode === newLineMode)
+ return;
+
+ this.$newLineMode = newLineMode;
+ this._signal("changeNewLineMode");
+ };
+ this.getNewLineMode = function() {
+ return this.$newLineMode;
+ };
+ this.isNewLine = function(text) {
+ return (text == "\r\n" || text == "\r" || text == "\n");
+ };
+ this.getLine = function(row) {
+ return this.$lines[row] || "";
+ };
+ this.getLines = function(firstRow, lastRow) {
+ return this.$lines.slice(firstRow, lastRow + 1);
+ };
+ this.getAllLines = function() {
+ return this.getLines(0, this.getLength());
+ };
+ this.getLength = function() {
+ return this.$lines.length;
+ };
+ this.getTextRange = function(range) {
+ return this.getLinesForRange(range).join(this.getNewLineCharacter());
+ };
+ this.getLinesForRange = function(range) {
+ var lines;
+ if (range.start.row === range.end.row) {
+ lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)];
+ } else {
+ lines = this.getLines(range.start.row, range.end.row);
+ lines[0] = (lines[0] || "").substring(range.start.column);
+ var l = lines.length - 1;
+ if (range.end.row - range.start.row == l)
+ lines[l] = lines[l].substring(0, range.end.column);
+ }
+ return lines;
+ };
+ this.insertLines = function(row, lines) {
+ console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead.");
+ return this.insertFullLines(row, lines);
+ };
+ this.removeLines = function(firstRow, lastRow) {
+ console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead.");
+ return this.removeFullLines(firstRow, lastRow);
+ };
+ this.insertNewLine = function(position) {
+ console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead.");
+ return this.insertMergedLines(position, ["", ""]);
+ };
+ this.insert = function(position, text) {
+ if (this.getLength() <= 1)
+ this.$detectNewLine(text);
+
+ return this.insertMergedLines(position, this.$split(text));
+ };
+ this.insertInLine = function(position, text) {
+ var start = this.clippedPos(position.row, position.column);
+ var end = this.pos(position.row, position.column + text.length);
+
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "insert",
+ lines: [text]
+ }, true);
+
+ return this.clonePos(end);
+ };
+
+ this.clippedPos = function(row, column) {
+ var length = this.getLength();
+ if (row === undefined) {
+ row = length;
+ } else if (row < 0) {
+ row = 0;
+ } else if (row >= length) {
+ row = length - 1;
+ column = undefined;
+ }
+ var line = this.getLine(row);
+ if (column == undefined)
+ column = line.length;
+ column = Math.min(Math.max(column, 0), line.length);
+ return {row: row, column: column};
+ };
+
+ this.clonePos = function(pos) {
+ return {row: pos.row, column: pos.column};
+ };
+
+ this.pos = function(row, column) {
+ return {row: row, column: column};
+ };
+
+ this.$clipPosition = function(position) {
+ var length = this.getLength();
+ if (position.row >= length) {
+ position.row = Math.max(0, length - 1);
+ position.column = this.getLine(length - 1).length;
+ } else {
+ position.row = Math.max(0, position.row);
+ position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length);
+ }
+ return position;
+ };
+ this.insertFullLines = function(row, lines) {
+ row = Math.min(Math.max(row, 0), this.getLength());
+ var column = 0;
+ if (row < this.getLength()) {
+ lines = lines.concat([""]);
+ column = 0;
+ } else {
+ lines = [""].concat(lines);
+ row--;
+ column = this.$lines[row].length;
+ }
+ this.insertMergedLines({row: row, column: column}, lines);
+ };
+ this.insertMergedLines = function(position, lines) {
+ var start = this.clippedPos(position.row, position.column);
+ var end = {
+ row: start.row + lines.length - 1,
+ column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length
+ };
+
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "insert",
+ lines: lines
+ });
+
+ return this.clonePos(end);
+ };
+ this.remove = function(range) {
+ var start = this.clippedPos(range.start.row, range.start.column);
+ var end = this.clippedPos(range.end.row, range.end.column);
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "remove",
+ lines: this.getLinesForRange({start: start, end: end})
+ });
+ return this.clonePos(start);
+ };
+ this.removeInLine = function(row, startColumn, endColumn) {
+ var start = this.clippedPos(row, startColumn);
+ var end = this.clippedPos(row, endColumn);
+
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "remove",
+ lines: this.getLinesForRange({start: start, end: end})
+ }, true);
+
+ return this.clonePos(start);
+ };
+ this.removeFullLines = function(firstRow, lastRow) {
+ firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1);
+ lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1);
+ var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0;
+ var deleteLastNewLine = lastRow < this.getLength() - 1;
+ var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow );
+ var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 );
+ var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow );
+ var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length );
+ var range = new Range(startRow, startCol, endRow, endCol);
+ var deletedLines = this.$lines.slice(firstRow, lastRow + 1);
+
+ this.applyDelta({
+ start: range.start,
+ end: range.end,
+ action: "remove",
+ lines: this.getLinesForRange(range)
+ });
+ return deletedLines;
+ };
+ this.removeNewLine = function(row) {
+ if (row < this.getLength() - 1 && row >= 0) {
+ this.applyDelta({
+ start: this.pos(row, this.getLine(row).length),
+ end: this.pos(row + 1, 0),
+ action: "remove",
+ lines: ["", ""]
+ });
+ }
+ };
+ this.replace = function(range, text) {
+ if (!(range instanceof Range))
+ range = Range.fromPoints(range.start, range.end);
+ if (text.length === 0 && range.isEmpty())
+ return range.start;
+ if (text == this.getTextRange(range))
+ return range.end;
+
+ this.remove(range);
+ var end;
+ if (text) {
+ end = this.insert(range.start, text);
+ }
+ else {
+ end = range.start;
+ }
+
+ return end;
+ };
+ this.applyDeltas = function(deltas) {
+ for (var i=0; i=0; i--) {
+ this.revertDelta(deltas[i]);
+ }
+ };
+ this.applyDelta = function(delta, doNotValidate) {
+ var isInsert = delta.action == "insert";
+ if (isInsert ? delta.lines.length <= 1 && !delta.lines[0]
+ : !Range.comparePoints(delta.start, delta.end)) {
+ return;
+ }
+
+ if (isInsert && delta.lines.length > 20000)
+ this.$splitAndapplyLargeDelta(delta, 20000);
+ applyDelta(this.$lines, delta, doNotValidate);
+ this._signal("change", delta);
+ };
+
+ this.$splitAndapplyLargeDelta = function(delta, MAX) {
+ var lines = delta.lines;
+ var l = lines.length;
+ var row = delta.start.row;
+ var column = delta.start.column;
+ var from = 0, to = 0;
+ do {
+ from = to;
+ to += MAX - 1;
+ var chunk = lines.slice(from, to);
+ if (to > l) {
+ delta.lines = chunk;
+ delta.start.row = row + from;
+ delta.start.column = column;
+ break;
+ }
+ chunk.push("");
+ this.applyDelta({
+ start: this.pos(row + from, column),
+ end: this.pos(row + to, column = 0),
+ action: delta.action,
+ lines: chunk
+ }, true);
+ } while(true);
+ };
+ this.revertDelta = function(delta) {
+ this.applyDelta({
+ start: this.clonePos(delta.start),
+ end: this.clonePos(delta.end),
+ action: (delta.action == "insert" ? "remove" : "insert"),
+ lines: delta.lines.slice()
+ });
+ };
+ this.indexToPosition = function(index, startRow) {
+ var lines = this.$lines || this.getAllLines();
+ var newlineLength = this.getNewLineCharacter().length;
+ for (var i = startRow || 0, l = lines.length; i < l; i++) {
+ index -= lines[i].length + newlineLength;
+ if (index < 0)
+ return {row: i, column: index + lines[i].length + newlineLength};
+ }
+ return {row: l-1, column: lines[l-1].length};
+ };
+ this.positionToIndex = function(pos, startRow) {
+ var lines = this.$lines || this.getAllLines();
+ var newlineLength = this.getNewLineCharacter().length;
+ var index = 0;
+ var row = Math.min(pos.row, lines.length);
+ for (var i = startRow || 0; i < row; ++i)
+ index += lines[i].length + newlineLength;
+
+ return index + pos.column;
+ };
+
+}).call(Document.prototype);
+
+exports.Document = Document;
+});
+
+ace.define("ace/lib/lang",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.last = function(a) {
+ return a[a.length - 1];
+};
+
+exports.stringReverse = function(string) {
+ return string.split("").reverse().join("");
+};
+
+exports.stringRepeat = function (string, count) {
+ var result = '';
+ while (count > 0) {
+ if (count & 1)
+ result += string;
+
+ if (count >>= 1)
+ string += string;
+ }
+ return result;
+};
+
+var trimBeginRegexp = /^\s\s*/;
+var trimEndRegexp = /\s\s*$/;
+
+exports.stringTrimLeft = function (string) {
+ return string.replace(trimBeginRegexp, '');
+};
+
+exports.stringTrimRight = function (string) {
+ return string.replace(trimEndRegexp, '');
+};
+
+exports.copyObject = function(obj) {
+ var copy = {};
+ for (var key in obj) {
+ copy[key] = obj[key];
+ }
+ return copy;
+};
+
+exports.copyArray = function(array){
+ var copy = [];
+ for (var i=0, l=array.length; i MAX_TOKENS) {
+ throw new Error("exceed max token count of " + MAX_TOKENS);
+ break;
+ };
+ var result = SPECIAL.exec(text);
+ if (result == null) {
+ if (idx < text.length) {
+ Tokens.push([lineNumber, "Text", idx, text.length]);
+ }
+ break;
+ }
+ if (result && result.index <= pos) {
+ throw new Error("infinite loop in parsing");
+ break;
+ };
+ pos = result.index;
+ if (pos > idx) {
+ Tokens.push([lineNumber, "Text", idx, pos]);
+ }
+ for (var i = idx; i < pos; i++) {
+ if (text[i] === "\n") {
+ lineNumber++;
+ linePosition[lineNumber] = i+1;
+ }
+ }
+
+ var newIdx = SPECIAL.lastIndex;
+ idx = newIdx;
+ var code = result[0];
+ if (code === "%") { // comment character
+ var newLinePos = text.indexOf("\n", idx);
+ if (newLinePos === -1) {
+ newLinePos = text.length;
+ };
+ var commentString = text.substring(idx, newLinePos);
+ if (commentString.indexOf("%novalidate") === 0) {
+ return [];
+ } else if(!checkingDisabled && commentString.indexOf("%begin novalidate") === 0) {
+ checkingDisabled = true;
+ } else if (checkingDisabled && commentString.indexOf("%end novalidate") === 0) {
+ checkingDisabled = false;
+ };
+ idx = SPECIAL.lastIndex = newLinePos + 1;
+ Comments.push([lineNumber, idx, newLinePos]);
+ lineNumber++;
+ linePosition[lineNumber] = idx;
+ } else if (checkingDisabled) {
+ continue;
+ } else if (code === '\\') { // escape character
+ NEXTCS.lastIndex = idx;
+ var controlSequence = NEXTCS.exec(text);
+ var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
+ if (nextSpecialPos === idx) {
+ Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]);
+ idx = SPECIAL.lastIndex = idx + 1;
+ char = text[nextSpecialPos];
+ if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
+ } else {
+ Tokens.push([lineNumber, code, pos, nextSpecialPos, text.slice(idx, nextSpecialPos)]);
+ var char;
+ while ((char = text[nextSpecialPos]) === ' ' || char === '\t' || char === '\r' || char === '\n') {
+ nextSpecialPos++;
+ if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
+ }
+ idx = SPECIAL.lastIndex = nextSpecialPos;
+ }
+ } else if (code === "{") { // open group
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "}") { // close group
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "$") { // math mode
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "&") { // tabalign
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "#") { // macro parameter
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "^") { // superscript
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "_") { // subscript
+ Tokens.push([lineNumber, code, pos]);
+ } else if (code === "~") { // active character (space)
+ Tokens.push([lineNumber, code, pos]);
+ } else {
+ throw "unrecognised character " + code;
+ }
+ }
+
+ return {tokens: Tokens, comments: Comments, linePosition: linePosition, lineNumber: lineNumber, text: text};
+};
+
+var read1arg = function (TokeniseResult, k, options) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+ if (options && options.allowStar) {
+ var optional = Tokens[k+1];
+ if (optional && optional[1] === "Text") {
+ var optionalstr = text.substring(optional[2], optional[3]);
+ if (optionalstr === "*") { k++;}
+ };
+ };
+
+ var open = Tokens[k+1];
+ var env = Tokens[k+2];
+ var close = Tokens[k+3];
+ var envName;
+
+ if(open && open[1] === "\\") {
+ envName = open[4]; // array element 4 is command sequence
+ return k + 1;
+ } else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") {
+ envName = env[4]; // NOTE: if we were actually using this, keep track of * above
+ return k + 3; // array element 4 is command sequence
+ } else {
+ return null;
+ }
+};
+
+var readLetDefinition = function (TokeniseResult, k) {
+
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var first = Tokens[k+1];
+ var second = Tokens[k+2];
+ var third = Tokens[k+3];
+
+ if(first && first[1] === "\\" && second && second[1] === "\\") {
+ return k + 2;
+ } else if(first && first[1] === "\\" &&
+ second && second[1] === "Text" && text.substring(second[2], second[3]) === "=" &&
+ third && third[1] === "\\") {
+ return k + 3;
+ } else {
+ return null;
+ }
+};
+
+var read1name = function (TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var open = Tokens[k+1];
+ var env = Tokens[k+2];
+ var close = Tokens[k+3];
+
+ if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
+ var envName = text.substring(env[2], env[3]);
+ return k + 3;
+ } else if (open && open[1] === "{" && env && env[1] === "Text") {
+ envName = "";
+ for (var j = k + 2, tok; (tok = Tokens[j]); j++) {
+ if (tok[1] === "Text") {
+ var str = text.substring(tok[2], tok[3]);
+ if (!str.match(/^\S*$/)) { break; }
+ envName = envName + str;
+ } else if (tok[1] === "_") {
+ envName = envName + "_";
+ } else {
+ break;
+ }
+ }
+ if (tok && tok[1] === "}") {
+ return j; // advance past these tokens
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+};
+
+var read1filename = function (TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var fileName = "";
+ for (var j = k + 1, tok; (tok = Tokens[j]); j++) {
+ if (tok[1] === "Text") {
+ var str = text.substring(tok[2], tok[3]);
+ if (!str.match(/^\S*$/)) { break; }
+ fileName = fileName + str;
+ } else if (tok[1] === "_") {
+ fileName = fileName + "_";
+ } else {
+ break;
+ }
+ }
+ if (fileName.length > 0) {
+ return j; // advance past these tokens
+ } else {
+ return null;
+ }
+};
+
+var readOptionalParams = function(TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var params = Tokens[k+1];
+
+ if(params && params[1] === "Text") {
+ var paramNum = text.substring(params[2], params[3]);
+ if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
+ return k + 1; // got it
+ };
+ };
+ var count = 0;
+ var nextToken = Tokens[k+1];
+ var pos = nextToken[2];
+
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === "[") { count++; }
+ if (char === "]") { count--; }
+ if (count === 0 && char === "{") { return k - 1; }
+ if (count > 0 && (char === '\r' || char === '\n')) { return null; }
+ };
+ return null;
+};
+
+var readOptionalGeneric = function(TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var params = Tokens[k+1];
+
+ if(params && params[1] === "Text") {
+ var paramNum = text.substring(params[2], params[3]);
+ if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) {
+ return k + 1; // got it
+ };
+ };
+ return null;
+};
+
+var readOptionalDef = function (TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var defToken = Tokens[k];
+ var pos = defToken[3];
+
+ var openBrace = "{";
+ var nextToken = Tokens[k+1];
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments
+ if (char === '\r' || char === '\n') { return null; }
+ };
+
+ return null;
+
+};
+
+var readDefinition = function(TokeniseResult, k) {
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ k = k + 1;
+ var count = 0;
+ var nextToken = Tokens[k];
+ while (nextToken && nextToken[1] === "Text") {
+ var start = nextToken[2], end = nextToken[3];
+ for (var i = start; i < end; i++) {
+ var char = text[i];
+ if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { continue; }
+ return null; // bail out, should begin with a {
+ }
+ k++;
+ nextToken = Tokens[k];
+ }
+ if (nextToken && nextToken[1] === "{") {
+ count++;
+ while (count>0) {
+ k++;
+ nextToken = Tokens[k];
+ if(!nextToken) { break; };
+ if (nextToken[1] === "}") { count--; }
+ if (nextToken[1] === "{") { count++; }
+ }
+ return k;
+ }
+
+ return null;
+};
+
+var readVerb = function(TokeniseResult, k) {
+
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var verbToken = Tokens[k];
+ var verbStr = text.substring(verbToken[2], verbToken[3]);
+ var pos = verbToken[3];
+ if (text[pos] === "*") { pos++; } // \verb* form of command
+ var delimiter = text[pos];
+ pos++;
+
+ var nextToken = Tokens[k+1];
+ for (var i = pos, end = text.length; i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === delimiter) { return k; };
+ if (char === '\r' || char === '\n') { return null; }
+ };
+
+ return null;
+};
+
+var readUrl = function(TokeniseResult, k) {
+
+ var Tokens = TokeniseResult.tokens;
+ var text = TokeniseResult.text;
+
+ var urlToken = Tokens[k];
+ var urlStr = text.substring(urlToken[2], urlToken[3]);
+ var pos = urlToken[3];
+ var openDelimiter = text[pos];
+ var closeDelimiter = (openDelimiter === "{") ? "}" : openDelimiter;
+ var nextToken = Tokens[k+1];
+ if (nextToken && pos === nextToken[2]) {
+ k++;
+ nextToken = Tokens[k+1];
+ };
+ pos++;
+
+ var count = 1;
+ for (var i = pos, end = text.length; count > 0 && i < end; i++) {
+ var char = text[i];
+ if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+ if (char === closeDelimiter) {
+ count--;
+ } else if (char === openDelimiter) {
+ count++;
+ };
+ if (count === 0) { return k; };
+ if (char === '\r' || char === '\n') { return null; }
+ };
+
+ return null;
+};
+
+var InterpretTokens = function (TokeniseResult, ErrorReporter) {
+ var Tokens = TokeniseResult.tokens;
+ var linePosition = TokeniseResult.linePosition;
+ var lineNumber = TokeniseResult.lineNumber;
+ var text = TokeniseResult.text;
+
+ var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
+ var TokenError = ErrorReporter.TokenError;
+ var Environments = new EnvHandler(ErrorReporter);
+
+ var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
+ var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes
+ var seenUserDefinedBeginEquation = false; // if we have seen macros like \beq
+ var seenUserDefinedEndEquation = false; // if we have seen macros like \eeq
+
+ for (var i = 0, len = Tokens.length; i < len; i++) {
+ var token = Tokens[i];
+ var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
+
+ if (type === "{") {
+ Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
+ nextGroupMathModeStack.push(nextGroupMathMode);
+ nextGroupMathMode = null;
+ continue;
+ } else if (type === "}") {
+ Environments.push({command:"}", token:token});
+ nextGroupMathMode = nextGroupMathModeStack.pop();
+ continue;
+ } else {
+ nextGroupMathMode = null;
+ };
+
+ if (type === "\\") {
+ if (seq === "begin" || seq === "end") {
+ var open = Tokens[i+1];
+ var env = Tokens[i+2];
+ var close = Tokens[i+3];
+ if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") {
+ var envName = text.substring(env[2], env[3]);
+ Environments.push({command: seq, name: envName, token: token, closeToken: close});
+ i = i + 3; // advance past these tokens
+ } else {
+ if (open && open[1] === "{" && env && env[1] === "Text") {
+ envName = "";
+ for (var j = i + 2, tok; (tok = Tokens[j]); j++) {
+ if (tok[1] === "Text") {
+ var str = text.substring(tok[2], tok[3]);
+ if (!str.match(/^\S*$/)) { break; }
+ envName = envName + str;
+ } else if (tok[1] === "_") {
+ envName = envName + "_";
+ } else {
+ break;
+ }
+ }
+ if (tok && tok[1] === "}") {
+ Environments.push({command: seq, name: envName, token: token, closeToken: close});
+ i = j; // advance past these tokens
+ continue;
+ }
+ }
+ var endToken = null;
+ if (open && open[1] === "{") {
+ endToken = open; // we've got a {
+ if (env && env[1] === "Text") {
+ endToken = env.slice(); // we've got some text following the {
+ start = endToken[2]; end = endToken[3];
+ for (j = start; j < end; j++) {
+ var char = text[j];
+ if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { break; }
+ }
+ endToken[3] = j; // the end of partial token is as far as we got looking ahead
+ };
+ };
+
+ if (endToken) {
+ TokenErrorFromTo(token, endToken, "invalid environment command " + text.substring(token[2], endToken[3] || endToken[2]));
+ } else {
+ TokenError(token, "invalid environment command");
+ };
+ }
+ } else if (typeof seq === "string" && seq.match(/^(be|beq|beqa|bea)$/i)) {
+ seenUserDefinedBeginEquation = true;
+ } else if (typeof seq === "string" && seq.match(/^(ee|eeq|eeqn|eeqa|eeqan|eea)$/i)) {
+ seenUserDefinedEndEquation = true;
+ } else if (seq === "newcommand" || seq === "renewcommand" || seq === "DeclareRobustCommand") {
+ var newPos = read1arg(TokeniseResult, i, {allowStar: true});
+ if (newPos === null) { continue; } else {i = newPos;};
+ newPos = readOptionalParams(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ } else if (seq === "def") {
+ newPos = read1arg(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ newPos = readOptionalDef(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ } else if (seq === "let") {
+ newPos = readLetDefinition(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+
+ } else if (seq === "newcolumntype") {
+ newPos = read1name(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ newPos = readOptionalParams(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ } else if (seq === "newenvironment" || seq === "renewenvironment") {
+ newPos = read1name(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ newPos = readOptionalParams(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (seq === "verb") {
+ newPos = readVerb(TokeniseResult, i);
+ if (newPos === null) { TokenError(token, "invalid verbatim command"); } else {i = newPos;};
+ } else if (seq === "url") {
+ newPos = readUrl(TokeniseResult, i);
+ if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;};
+ } else if (seq === "left" || seq === "right") {
+ var nextToken = Tokens[i+1];
+ char = "";
+ if (nextToken && nextToken[1] === "Text") {
+ char = text.substring(nextToken[2], nextToken[2] + 1);
+ } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") {
+ char = nextToken[4];
+ } else if (nextToken && nextToken[1] === "\\") {
+ char = "unknown";
+ }
+ if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) {
+ TokenError(token, "invalid bracket command");
+ } else {
+ i = i + 1;
+ Environments.push({command:seq, token:token});
+ };
+ } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") {
+ Environments.push({command:seq, token:token});
+ } else if (seq === "input") {
+ newPos = read1filename(TokeniseResult, i);
+ if (newPos === null) { continue; } else {i = newPos;};
+ } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") {
+ nextGroupMathMode = false;
+ } else if (seq === "rotatebox" || seq === "scalebox") {
+ newPos = readOptionalGeneric(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ nextGroupMathMode = false;
+ } else if (seq === "resizebox") {
+ newPos = readOptionalGeneric(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+ nextGroupMathMode = false;
+ } else if (seq === "DeclareMathOperator") {
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (seq === "DeclarePairedDelimiter") {
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ newPos = readDefinition(TokeniseResult, i);
+ if (newPos === null) { /* do nothing */ } else {i = newPos;};
+ } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) {
+ var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ if (currentMathMode === null) {
+ TokenError(token, type + seq + " must be inside math mode", {mathMode:true});
+ };
+ } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) {
+ currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ if (currentMathMode) {
+ TokenError(token, type + seq + " used inside math mode", {mathMode:true});
+ Environments.resetMathMode();
+ };
+ } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) {
+ nextGroupMathMode = undefined;
+ };
+
+ } else if (type === "$") {
+ var lookAhead = Tokens[i+1];
+ var nextIsDollar = lookAhead && lookAhead[1] === "$";
+ currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) {
+ Environments.push({command:"$$", token:token});
+ i = i + 1;
+ } else {
+ Environments.push({command:"$", token:token});
+ }
+ } else if (type === "^" || type === "_") {
+ currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ var insideGroup = Environments.insideGroup(); // true if inside {....}
+ if (currentMathMode === null && !insideGroup) {
+ TokenError(token, type + " must be inside math mode", {mathMode:true});
+ };
+ }
+ };
+
+ if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) {
+ ErrorReporter.filterMath = true;
+ };
+
+ return Environments;
+};
+
+var EnvHandler = function (ErrorReporter) {
+ var ErrorTo = ErrorReporter.EnvErrorTo;
+ var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
+ var ErrorFrom = ErrorReporter.EnvErrorFrom;
+
+ var envs = [];
+
+ var state = [];
+ var documentClosed = null;
+ var inVerbatim = false;
+ var verbatimRanges = [];
+
+ this.Environments = envs;
+
+ this.push = function (newEnv) {
+ this.setEnvProps(newEnv);
+ this.checkAndUpdateState(newEnv);
+ envs.push(newEnv);
+ };
+
+ this._endVerbatim = function (thisEnv) {
+ var lastEnv = state.pop();
+ if (lastEnv && lastEnv.name === thisEnv.name) {
+ inVerbatim = false;
+ verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
+ } else {
+ if(lastEnv) { state.push(lastEnv); } ;
+ }
+ };
+
+ var invalidEnvs = [];
+
+ this._end = function (thisEnv) {
+ do {
+ var lastEnv = state.pop();
+ var retry = false;
+ var i;
+
+ if (closedBy(lastEnv, thisEnv)) {
+ if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
+ documentClosed = thisEnv;
+ };
+ return;
+ } else if (!lastEnv) {
+ if (documentClosed) {
+ ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+ } else {
+ ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
+ }
+ } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
+ invalidEnvs.splice(i, 1);
+ if (lastEnv) { state.push(lastEnv); } ;
+ return;
+ } else {
+ var status = reportError(lastEnv, thisEnv);
+ if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) {
+ invalidEnvs.push(lastEnv);
+ retry = true;
+ } else {
+ var prevLastEnv = state.pop();
+ if(prevLastEnv) {
+ if (thisEnv.name === prevLastEnv.name) {
+ return;
+ } else {
+ state.push(prevLastEnv);
+ }
+ }
+ invalidEnvs.push(lastEnv);
+ }
+
+ }
+ } while (retry === true);
+ };
+
+ var CLOSING_DELIMITER = {
+ "{" : "}",
+ "left" : "right",
+ "[" : "]",
+ "(" : ")",
+ "$" : "$",
+ "$$": "$$"
+ };
+
+ var closedBy = function (lastEnv, thisEnv) {
+ if (!lastEnv) {
+ return false ;
+ } else if (thisEnv.command === "end") {
+ return lastEnv.command === "begin" && lastEnv.name === thisEnv.name;
+ } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ var indexOfClosingEnvInArray = function (envs, thisEnv) {
+ for (var i = 0, n = envs.length; i < n ; i++) {
+ if (closedBy(envs[i], thisEnv)) {
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ var envPrecedence = function (env) {
+ var openScore = {
+ "{" : 1,
+ "left" : 2,
+ "$" : 3,
+ "$$" : 4,
+ "begin": 4
+ };
+ var closeScore = {
+ "}" : 1,
+ "right" : 2,
+ "$" : 3,
+ "$$" : 5,
+ "end": 4
+ };
+ if (env.command) {
+ return openScore[env.command] || closeScore[env.command];
+ } else {
+ return 0;
+ }
+ };
+
+ var getName = function(env) {
+ var description = {
+ "{" : "open group {",
+ "}" : "close group }",
+ "[" : "open display math \\[",
+ "]" : "close display math \\]",
+ "(" : "open inline math \\(",
+ ")" : "close inline math \\)",
+ "$" : "$",
+ "$$" : "$$",
+ "left" : "\\left",
+ "right" : "\\right"
+ };
+ if (env.command === "begin" || env.command === "end") {
+ return "\\" + env.command + "{" + env.name + "}";
+ } else if (env.command in description) {
+ return description[env.command];
+ } else {
+ return env.command;
+ }
+ };
+
+ var EXTRA_CLOSE = 1;
+ var UNCLOSED_GROUP = 2;
+ var UNCLOSED_ENV = 3;
+
+ var reportError = function(lastEnv, thisEnv) {
+ if (!lastEnv) { // unexpected close, nothing was open!
+ if (documentClosed) {
+ ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
+ } else {
+ ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
+ };
+ return EXTRA_CLOSE;
+ } else if (lastEnv.command === "{" && thisEnv.command === "end") {
+ ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
+ {suppressIfEditing:true, errorAtStart: true, type:"warning"});
+ return UNCLOSED_GROUP;
+ } else {
+ var pLast = envPrecedence(lastEnv);
+ var pThis = envPrecedence(thisEnv);
+ if (pThis > pLast) {
+ ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
+ {suppressIfEditing:true, errorAtStart: true});
+ } else {
+ ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv));
+ }
+ return UNCLOSED_ENV;
+ };
+ };
+
+ this._beginMathMode = function (thisEnv) {
+ var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
+ if (currentMathMode) {
+ ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
+ {suppressIfEditing:true, errorAtStart: true, mathMode:true});
+ };
+ thisEnv.mathMode = thisEnv;
+ state.push(thisEnv);
+ };
+
+ this._toggleMathMode = function (thisEnv) {
+ var lastEnv = state.pop();
+ if (closedBy(lastEnv, thisEnv)) {
+ return;
+ } else {
+ if (lastEnv) {state.push(lastEnv);}
+ if (lastEnv && lastEnv.mathMode) {
+ this._end(thisEnv);
+ } else {
+ thisEnv.mathMode = thisEnv;
+ state.push(thisEnv);
+ }
+ };
+ };
+
+ this.getMathMode = function () {
+ var n = state.length;
+ if (n > 0) {
+ return state[n-1].mathMode;
+ } else {
+ return null;
+ }
+ };
+
+ this.insideGroup = function () {
+ var n = state.length;
+ if (n > 0) {
+ return (state[n-1].command === "{");
+ } else {
+ return null;
+ }
+ };
+
+ var resetMathMode = function () {
+ var n = state.length;
+ if (n > 0) {
+ var lastMathMode = state[n-1].mathMode;
+ do {
+ var lastEnv = state.pop();
+ } while (lastEnv && lastEnv !== lastMathMode);
+ } else {
+ return;
+ }
+ };
+
+ this.resetMathMode = resetMathMode;
+
+ var getNewMathMode = function (currentMathMode, thisEnv) {
+ var newMathMode = null;
+
+ if (thisEnv.command === "{") {
+ if (thisEnv.mathMode !== null) {
+ newMathMode = thisEnv.mathMode;
+ } else {
+ newMathMode = currentMathMode;
+ }
+ } else if (thisEnv.command === "left") {
+ if (currentMathMode === null) {
+ ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true});
+ };
+ newMathMode = currentMathMode;
+ } else if (thisEnv.command === "begin") {
+ var name = thisEnv.name;
+ if (name) {
+ if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
+ if (currentMathMode) {
+ ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
+ {suppressIfEditing:true, errorAtStart: true, mathMode: true});
+ resetMathMode();
+ };
+ newMathMode = null;
+ } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) {
+ if (currentMathMode === null) {
+ ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true});
+ };
+ newMathMode = currentMathMode;
+ } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
+ if (currentMathMode) {
+ ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
+ {suppressIfEditing:true, errorAtStart: true, mathMode: true});
+ resetMathMode();
+ };
+ newMathMode = thisEnv;
+ } else {
+ newMathMode = undefined; // undefined means we don't know if we are in math mode or not
+ }
+ }
+ };
+ return newMathMode;
+ };
+
+ this.checkAndUpdateState = function (thisEnv) {
+ if (inVerbatim) {
+ if (thisEnv.command === "end") {
+ this._endVerbatim(thisEnv);
+ } else {
+ return; // ignore anything in verbatim environments
+ }
+ } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") {
+ if (thisEnv.verbatim) {inVerbatim = true;};
+ var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
+ var newMathMode = getNewMathMode(currentMathMode, thisEnv);
+ thisEnv.mathMode = newMathMode;
+ state.push(thisEnv);
+ } else if (thisEnv.command === "end") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "(" || thisEnv.command === "[") {
+ this._beginMathMode(thisEnv);
+ } else if (thisEnv.command === ")" || thisEnv.command === "]") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "}") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "right") {
+ this._end(thisEnv);
+ } else if (thisEnv.command === "$" || thisEnv.command === "$$") {
+ this._toggleMathMode(thisEnv);
+ }
+ };
+
+ this.close = function () {
+ while (state.length > 0) {
+ var thisEnv = state.pop();
+ if (thisEnv.command === "{") {
+ ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
+ } else {
+ ErrorFrom(thisEnv, "unclosed " + getName(thisEnv));
+ }
+ }
+ var vlen = verbatimRanges.length;
+ var len = ErrorReporter.tokenErrors.length;
+ if (vlen >0 && len > 0) {
+ for (var i = 0; i < len; i++) {
+ var tokenError = ErrorReporter.tokenErrors[i];
+ var startPos = tokenError.startPos;
+ var endPos = tokenError.endPos;
+ for (var j = 0; j < vlen; j++) {
+ if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
+ tokenError.ignore = true;
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ this.setEnvProps = function (env) {
+ var name = env.name ;
+ if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) {
+ env.verbatim = true;
+ }
+ };
+};
+var ErrorReporter = function (TokeniseResult) {
+ var text = TokeniseResult.text;
+ var linePosition = TokeniseResult.linePosition;
+ var lineNumber = TokeniseResult.lineNumber;
+
+ var errors = [], tokenErrors = [];
+ this.errors = errors;
+ this.tokenErrors = tokenErrors;
+ this.filterMath = false;
+
+ this.getErrors = function () {
+ var returnedErrors = [];
+ for (var i = 0, len = tokenErrors.length; i < len; i++) {
+ if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
+ }
+ var allErrors = returnedErrors.concat(errors);
+ var result = [];
+
+ var mathErrorCount = 0;
+ for (i = 0, len = allErrors.length; i < len; i++) {
+ if (allErrors[i].mathMode) {
+ mathErrorCount++;
+ }
+ if (mathErrorCount > 10) {
+ return [];
+ }
+ }
+
+ if (this.filterMath && mathErrorCount > 0) {
+ for (i = 0, len = allErrors.length; i < len; i++) {
+ if (!allErrors[i].mathMode) {
+ result.push(allErrors[i]);
+ }
+ }
+ return result;
+ } else {
+ return allErrors;
+ }
+ };
+
+ this.TokenError = function (token, message, options) {
+ if(!options) { options = { suppressIfEditing:true } ; };
+ var line = token[0], type = token[1], start = token[2], end = token[3];
+ var start_col = start - linePosition[line];
+ if (!end) { end = start + 1; } ;
+ var end_col = end - linePosition[line];
+ tokenErrors.push({row: line,
+ column: start_col,
+ start_row:line,
+ start_col: start_col,
+ end_row:line,
+ end_col: end_col,
+ type:"error",
+ text:message,
+ startPos: start,
+ endPos: end,
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
+ };
+
+ this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
+ if(!options) { options = {suppressIfEditing:true } ; };
+ var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
+ var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
+ if (!toEnd) { toEnd = toStart + 1;};
+ var start_col = fromStart - linePosition[fromLine];
+ var end_col = toEnd - linePosition[toLine];
+
+ tokenErrors.push({row: fromLine,
+ column: start_col,
+ start_row: fromLine,
+ start_col: start_col,
+ end_row: toLine,
+ end_col: end_col,
+ type:"error",
+ text:message,
+ startPos: fromStart,
+ endPos: toEnd,
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
+ };
+
+
+ this.EnvErrorFromTo = function (fromEnv, toEnv, message, options) {
+ if(!options) { options = {} ; };
+ var fromToken = fromEnv.token, toToken = toEnv.closeToken || toEnv.token;
+ var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
+ if (!toToken) {toToken = fromToken;};
+ var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
+ if (!toEnd) { toEnd = toStart + 1;};
+ var start_col = fromStart - linePosition[fromLine];
+ var end_col = toEnd - linePosition[toLine];
+ errors.push({row: options.errorAtStart ? fromLine : toLine,
+ column: options.errorAtStart ? start_col: end_col,
+ start_row:fromLine,
+ start_col: start_col,
+ end_row:toLine,
+ end_col: end_col,
+ type: options.type ? options.type : "error",
+ text:message,
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
+ };
+
+ this.EnvErrorTo = function (toEnv, message, options) {
+ if(!options) { options = {} ; };
+ var token = toEnv.closeToken || toEnv.token;
+ var line = token[0], type = token[1], start = token[2], end = token[3];
+ if (!end) { end = start + 1; };
+ var end_col = end - linePosition[line];
+ var err = {row: line,
+ column: end_col,
+ start_row:0,
+ start_col: 0,
+ end_row: line,
+ end_col: end_col,
+ type: options.type ? options.type : "error",
+ text:message,
+ mathMode: options.mathMode};
+ errors.push(err);
+ };
+
+ this.EnvErrorFrom = function (env, message, options) {
+ if(!options) { options = {} ; };
+ var token = env.token;
+ var line = token[0], type = token[1], start = token[2], end = token[3];
+ var start_col = start - linePosition[line];
+ var end_col = Infinity;
+ errors.push({row: line,
+ column: start_col,
+ start_row:line,
+ start_col: start_col,
+ end_row: lineNumber,
+ end_col: end_col,
+ type: options.type ? options.type : "error",
+ text:message,
+ mathMode: options.mathMode});
+ };
+};
+
+var Parse = function (text) {
+ var TokeniseResult = Tokenise(text);
+ var Reporter = new ErrorReporter(TokeniseResult);
+ var Environments = InterpretTokens(TokeniseResult, Reporter);
+ Environments.close();
+ return Reporter.getErrors();
+};
+
+(function() {
+ var disabled = false;
+
+ this.onUpdate = function() {
+ if (disabled) { return ; };
+
+ var value = this.doc.getValue();
+ var errors = [];
+ try {
+ if (value)
+ errors = Parse(value);
+ } catch (e) {
+ disabled = true;
+ errors = [];
+ }
+ this.sender.emit("lint", errors);
+ };
+
+}).call(LatexWorker.prototype);
+
+});
+
+ace.define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) {
+
+function Empty() {}
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function bind(that) { // .length is 1
+ var target = this;
+ if (typeof target != "function") {
+ throw new TypeError("Function.prototype.bind called on incompatible " + target);
+ }
+ var args = slice.call(arguments, 1); // for normal call
+ var bound = function () {
+
+ if (this instanceof bound) {
+
+ var result = target.apply(
+ this,
+ args.concat(slice.call(arguments))
+ );
+ if (Object(result) === result) {
+ return result;
+ }
+ return this;
+
+ } else {
+ return target.apply(
+ that,
+ args.concat(slice.call(arguments))
+ );
+
+ }
+
+ };
+ if(target.prototype) {
+ Empty.prototype = target.prototype;
+ bound.prototype = new Empty();
+ Empty.prototype = null;
+ }
+ return bound;
+ };
+}
+var call = Function.prototype.call;
+var prototypeOfArray = Array.prototype;
+var prototypeOfObject = Object.prototype;
+var slice = prototypeOfArray.slice;
+var _toString = call.bind(prototypeOfObject.toString);
+var owns = call.bind(prototypeOfObject.hasOwnProperty);
+var defineGetter;
+var defineSetter;
+var lookupGetter;
+var lookupSetter;
+var supportsAccessors;
+if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) {
+ defineGetter = call.bind(prototypeOfObject.__defineGetter__);
+ defineSetter = call.bind(prototypeOfObject.__defineSetter__);
+ lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);
+ lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);
+}
+if ([1,2].splice(0).length != 2) {
+ if(function() { // test IE < 9 to splice bug - see issue #138
+ function makeArray(l) {
+ var a = new Array(l+2);
+ a[0] = a[1] = 0;
+ return a;
+ }
+ var array = [], lengthBefore;
+
+ array.splice.apply(array, makeArray(20));
+ array.splice.apply(array, makeArray(26));
+
+ lengthBefore = array.length; //46
+ array.splice(5, 0, "XXX"); // add one element
+
+ lengthBefore + 1 == array.length
+
+ if (lengthBefore + 1 == array.length) {
+ return true;// has right splice implementation without bugs
+ }
+ }()) {//IE 6/7
+ var array_splice = Array.prototype.splice;
+ Array.prototype.splice = function(start, deleteCount) {
+ if (!arguments.length) {
+ return [];
+ } else {
+ return array_splice.apply(this, [
+ start === void 0 ? 0 : start,
+ deleteCount === void 0 ? (this.length - start) : deleteCount
+ ].concat(slice.call(arguments, 2)))
+ }
+ };
+ } else {//IE8
+ Array.prototype.splice = function(pos, removeCount){
+ var length = this.length;
+ if (pos > 0) {
+ if (pos > length)
+ pos = length;
+ } else if (pos == void 0) {
+ pos = 0;
+ } else if (pos < 0) {
+ pos = Math.max(length + pos, 0);
+ }
+
+ if (!(pos+removeCount < length))
+ removeCount = length - pos;
+
+ var removed = this.slice(pos, pos+removeCount);
+ var insert = slice.call(arguments, 2);
+ var add = insert.length;
+ if (pos === length) {
+ if (add) {
+ this.push.apply(this, insert);
+ }
+ } else {
+ var remove = Math.min(removeCount, length - pos);
+ var tailOldPos = pos + remove;
+ var tailNewPos = tailOldPos + add - remove;
+ var tailCount = length - tailOldPos;
+ var lengthAfterRemove = length - remove;
+
+ if (tailNewPos < tailOldPos) { // case A
+ for (var i = 0; i < tailCount; ++i) {
+ this[tailNewPos+i] = this[tailOldPos+i];
+ }
+ } else if (tailNewPos > tailOldPos) { // case B
+ for (i = tailCount; i--; ) {
+ this[tailNewPos+i] = this[tailOldPos+i];
+ }
+ } // else, add == remove (nothing to do)
+
+ if (add && pos === lengthAfterRemove) {
+ this.length = lengthAfterRemove; // truncate array
+ this.push.apply(this, insert);
+ } else {
+ this.length = lengthAfterRemove + add; // reserves space
+ for (i = 0; i < add; ++i) {
+ this[pos+i] = insert[i];
+ }
+ }
+ }
+ return removed;
+ };
+ }
+}
+if (!Array.isArray) {
+ Array.isArray = function isArray(obj) {
+ return _toString(obj) == "[object Array]";
+ };
+}
+var boxedString = Object("a"),
+ splitString = boxedString[0] != "a" || !(0 in boxedString);
+
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function forEach(fun /*, thisp*/) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ thisp = arguments[1],
+ i = -1,
+ length = self.length >>> 0;
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(); // TODO message
+ }
+
+ while (++i < length) {
+ if (i in self) {
+ fun.call(thisp, self[i], i, object);
+ }
+ }
+ };
+}
+if (!Array.prototype.map) {
+ Array.prototype.map = function map(fun /*, thisp*/) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ length = self.length >>> 0,
+ result = Array(length),
+ thisp = arguments[1];
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(fun + " is not a function");
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self)
+ result[i] = fun.call(thisp, self[i], i, object);
+ }
+ return result;
+ };
+}
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function filter(fun /*, thisp */) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ length = self.length >>> 0,
+ result = [],
+ value,
+ thisp = arguments[1];
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(fun + " is not a function");
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self) {
+ value = self[i];
+ if (fun.call(thisp, value, i, object)) {
+ result.push(value);
+ }
+ }
+ }
+ return result;
+ };
+}
+if (!Array.prototype.every) {
+ Array.prototype.every = function every(fun /*, thisp */) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ length = self.length >>> 0,
+ thisp = arguments[1];
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(fun + " is not a function");
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self && !fun.call(thisp, self[i], i, object)) {
+ return false;
+ }
+ }
+ return true;
+ };
+}
+if (!Array.prototype.some) {
+ Array.prototype.some = function some(fun /*, thisp */) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ length = self.length >>> 0,
+ thisp = arguments[1];
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(fun + " is not a function");
+ }
+
+ for (var i = 0; i < length; i++) {
+ if (i in self && fun.call(thisp, self[i], i, object)) {
+ return true;
+ }
+ }
+ return false;
+ };
+}
+if (!Array.prototype.reduce) {
+ Array.prototype.reduce = function reduce(fun /*, initial*/) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ length = self.length >>> 0;
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(fun + " is not a function");
+ }
+ if (!length && arguments.length == 1) {
+ throw new TypeError("reduce of empty array with no initial value");
+ }
+
+ var i = 0;
+ var result;
+ if (arguments.length >= 2) {
+ result = arguments[1];
+ } else {
+ do {
+ if (i in self) {
+ result = self[i++];
+ break;
+ }
+ if (++i >= length) {
+ throw new TypeError("reduce of empty array with no initial value");
+ }
+ } while (true);
+ }
+
+ for (; i < length; i++) {
+ if (i in self) {
+ result = fun.call(void 0, result, self[i], i, object);
+ }
+ }
+
+ return result;
+ };
+}
+if (!Array.prototype.reduceRight) {
+ Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {
+ var object = toObject(this),
+ self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ object,
+ length = self.length >>> 0;
+ if (_toString(fun) != "[object Function]") {
+ throw new TypeError(fun + " is not a function");
+ }
+ if (!length && arguments.length == 1) {
+ throw new TypeError("reduceRight of empty array with no initial value");
+ }
+
+ var result, i = length - 1;
+ if (arguments.length >= 2) {
+ result = arguments[1];
+ } else {
+ do {
+ if (i in self) {
+ result = self[i--];
+ break;
+ }
+ if (--i < 0) {
+ throw new TypeError("reduceRight of empty array with no initial value");
+ }
+ } while (true);
+ }
+
+ do {
+ if (i in this) {
+ result = fun.call(void 0, result, self[i], i, object);
+ }
+ } while (i--);
+
+ return result;
+ };
+}
+if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) {
+ Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) {
+ var self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ toObject(this),
+ length = self.length >>> 0;
+
+ if (!length) {
+ return -1;
+ }
+
+ var i = 0;
+ if (arguments.length > 1) {
+ i = toInteger(arguments[1]);
+ }
+ i = i >= 0 ? i : Math.max(0, length + i);
+ for (; i < length; i++) {
+ if (i in self && self[i] === sought) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) {
+ Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) {
+ var self = splitString && _toString(this) == "[object String]" ?
+ this.split("") :
+ toObject(this),
+ length = self.length >>> 0;
+
+ if (!length) {
+ return -1;
+ }
+ var i = length - 1;
+ if (arguments.length > 1) {
+ i = Math.min(i, toInteger(arguments[1]));
+ }
+ i = i >= 0 ? i : length - Math.abs(i);
+ for (; i >= 0; i--) {
+ if (i in self && sought === self[i]) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+if (!Object.getPrototypeOf) {
+ Object.getPrototypeOf = function getPrototypeOf(object) {
+ return object.__proto__ || (
+ object.constructor ?
+ object.constructor.prototype :
+ prototypeOfObject
+ );
+ };
+}
+if (!Object.getOwnPropertyDescriptor) {
+ var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " +
+ "non-object: ";
+ Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {
+ if ((typeof object != "object" && typeof object != "function") || object === null)
+ throw new TypeError(ERR_NON_OBJECT + object);
+ if (!owns(object, property))
+ return;
+
+ var descriptor, getter, setter;
+ descriptor = { enumerable: true, configurable: true };
+ if (supportsAccessors) {
+ var prototype = object.__proto__;
+ object.__proto__ = prototypeOfObject;
+
+ var getter = lookupGetter(object, property);
+ var setter = lookupSetter(object, property);
+ object.__proto__ = prototype;
+
+ if (getter || setter) {
+ if (getter) descriptor.get = getter;
+ if (setter) descriptor.set = setter;
+ return descriptor;
+ }
+ }
+ descriptor.value = object[property];
+ return descriptor;
+ };
+}
+if (!Object.getOwnPropertyNames) {
+ Object.getOwnPropertyNames = function getOwnPropertyNames(object) {
+ return Object.keys(object);
+ };
+}
+if (!Object.create) {
+ var createEmpty;
+ if (Object.prototype.__proto__ === null) {
+ createEmpty = function () {
+ return { "__proto__": null };
+ };
+ } else {
+ createEmpty = function () {
+ var empty = {};
+ for (var i in empty)
+ empty[i] = null;
+ empty.constructor =
+ empty.hasOwnProperty =
+ empty.propertyIsEnumerable =
+ empty.isPrototypeOf =
+ empty.toLocaleString =
+ empty.toString =
+ empty.valueOf =
+ empty.__proto__ = null;
+ return empty;
+ }
+ }
+
+ Object.create = function create(prototype, properties) {
+ var object;
+ if (prototype === null) {
+ object = createEmpty();
+ } else {
+ if (typeof prototype != "object")
+ throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'");
+ var Type = function () {};
+ Type.prototype = prototype;
+ object = new Type();
+ object.__proto__ = prototype;
+ }
+ if (properties !== void 0)
+ Object.defineProperties(object, properties);
+ return object;
+ };
+}
+
+function doesDefinePropertyWork(object) {
+ try {
+ Object.defineProperty(object, "sentinel", {});
+ return "sentinel" in object;
+ } catch (exception) {
+ }
+}
+if (Object.defineProperty) {
+ var definePropertyWorksOnObject = doesDefinePropertyWork({});
+ var definePropertyWorksOnDom = typeof document == "undefined" ||
+ doesDefinePropertyWork(document.createElement("div"));
+ if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) {
+ var definePropertyFallback = Object.defineProperty;
+ }
+}
+
+if (!Object.defineProperty || definePropertyFallback) {
+ var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ";
+ var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: "
+ var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " +
+ "on this javascript engine";
+
+ Object.defineProperty = function defineProperty(object, property, descriptor) {
+ if ((typeof object != "object" && typeof object != "function") || object === null)
+ throw new TypeError(ERR_NON_OBJECT_TARGET + object);
+ if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null)
+ throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
+ if (definePropertyFallback) {
+ try {
+ return definePropertyFallback.call(Object, object, property, descriptor);
+ } catch (exception) {
+ }
+ }
+ if (owns(descriptor, "value")) {
+
+ if (supportsAccessors && (lookupGetter(object, property) ||
+ lookupSetter(object, property)))
+ {
+ var prototype = object.__proto__;
+ object.__proto__ = prototypeOfObject;
+ delete object[property];
+ object[property] = descriptor.value;
+ object.__proto__ = prototype;
+ } else {
+ object[property] = descriptor.value;
+ }
+ } else {
+ if (!supportsAccessors)
+ throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
+ if (owns(descriptor, "get"))
+ defineGetter(object, property, descriptor.get);
+ if (owns(descriptor, "set"))
+ defineSetter(object, property, descriptor.set);
+ }
+
+ return object;
+ };
+}
+if (!Object.defineProperties) {
+ Object.defineProperties = function defineProperties(object, properties) {
+ for (var property in properties) {
+ if (owns(properties, property))
+ Object.defineProperty(object, property, properties[property]);
+ }
+ return object;
+ };
+}
+if (!Object.seal) {
+ Object.seal = function seal(object) {
+ return object;
+ };
+}
+if (!Object.freeze) {
+ Object.freeze = function freeze(object) {
+ return object;
+ };
+}
+try {
+ Object.freeze(function () {});
+} catch (exception) {
+ Object.freeze = (function freeze(freezeObject) {
+ return function freeze(object) {
+ if (typeof object == "function") {
+ return object;
+ } else {
+ return freezeObject(object);
+ }
+ };
+ })(Object.freeze);
+}
+if (!Object.preventExtensions) {
+ Object.preventExtensions = function preventExtensions(object) {
+ return object;
+ };
+}
+if (!Object.isSealed) {
+ Object.isSealed = function isSealed(object) {
+ return false;
+ };
+}
+if (!Object.isFrozen) {
+ Object.isFrozen = function isFrozen(object) {
+ return false;
+ };
+}
+if (!Object.isExtensible) {
+ Object.isExtensible = function isExtensible(object) {
+ if (Object(object) === object) {
+ throw new TypeError(); // TODO message
+ }
+ var name = '';
+ while (owns(object, name)) {
+ name += '?';
+ }
+ object[name] = true;
+ var returnValue = owns(object, name);
+ delete object[name];
+ return returnValue;
+ };
+}
+if (!Object.keys) {
+ var hasDontEnumBug = true,
+ dontEnums = [
+ "toString",
+ "toLocaleString",
+ "valueOf",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "constructor"
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ for (var key in {"toString": null}) {
+ hasDontEnumBug = false;
+ }
+
+ Object.keys = function keys(object) {
+
+ if (
+ (typeof object != "object" && typeof object != "function") ||
+ object === null
+ ) {
+ throw new TypeError("Object.keys called on a non-object");
+ }
+
+ var keys = [];
+ for (var name in object) {
+ if (owns(object, name)) {
+ keys.push(name);
+ }
+ }
+
+ if (hasDontEnumBug) {
+ for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
+ var dontEnum = dontEnums[i];
+ if (owns(object, dontEnum)) {
+ keys.push(dontEnum);
+ }
+ }
+ }
+ return keys;
+ };
+
+}
+if (!Date.now) {
+ Date.now = function now() {
+ return new Date().getTime();
+ };
+}
+var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
+ "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
+ "\u2029\uFEFF";
+if (!String.prototype.trim || ws.trim()) {
+ ws = "[" + ws + "]";
+ var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
+ trimEndRegexp = new RegExp(ws + ws + "*$");
+ String.prototype.trim = function trim() {
+ return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, "");
+ };
+}
+
+function toInteger(n) {
+ n = +n;
+ if (n !== n) { // isNaN
+ n = 0;
+ } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) {
+ n = (n > 0 || -1) * Math.floor(Math.abs(n));
+ }
+ return n;
+}
+
+function isPrimitive(input) {
+ var type = typeof input;
+ return (
+ input === null ||
+ type === "undefined" ||
+ type === "boolean" ||
+ type === "number" ||
+ type === "string"
+ );
+}
+
+function toPrimitive(input) {
+ var val, valueOf, toString;
+ if (isPrimitive(input)) {
+ return input;
+ }
+ valueOf = input.valueOf;
+ if (typeof valueOf === "function") {
+ val = valueOf.call(input);
+ if (isPrimitive(val)) {
+ return val;
+ }
+ }
+ toString = input.toString;
+ if (typeof toString === "function") {
+ val = toString.call(input);
+ if (isPrimitive(val)) {
+ return val;
+ }
+ }
+ throw new TypeError();
+}
+var toObject = function (o) {
+ if (o == null) { // this matches both null and undefined
+ throw new TypeError("can't convert "+o+" to object");
+ }
+ return Object(o);
+};
+
+});
diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less
index cd06a15313..9efa8fdbad 100644
--- a/services/web/public/stylesheets/app/editor/share.less
+++ b/services/web/public/stylesheets/app/editor/share.less
@@ -47,4 +47,10 @@
}
}
}
-}
\ No newline at end of file
+}
+.modal-footer-share {
+ .modal-footer-left {
+ max-width: 70%;
+ text-align: left;
+ }
+}
diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less
index de3307a017..5e03c1facc 100644
--- a/services/web/public/stylesheets/app/project-list.less
+++ b/services/web/public/stylesheets/app/project-list.less
@@ -1,3 +1,26 @@
+@announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
+
+@keyframes pulse {
+ 0% {
+ opacity: .7;
+ }
+ 100% {
+ opacity: .9;
+ }
+}
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.project-list-page {
+ position: relative;
+}
+
.project-header {
.btn-group > .btn {
padding-left: @line-height-base / 2;
@@ -293,3 +316,146 @@ ul.project-list {
margin-left:-100px;
}
}
+
+.announcements {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ height: 150px;
+ width: 100%;
+ pointer-events: none;
+ overflow: hidden;
+
+ &-open {
+ top: -100%;
+ height: auto;
+ pointer-events: all;
+ }
+}
+
+.announcements-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.35);
+ opacity: 0;
+ animation: fade-in 0.35s forwards;
+ z-index: 1;
+}
+
+.announcements-btn {
+ position: absolute;
+ bottom: -50px;
+ right: 3%;
+ width: 80px;
+ height: 80px;
+ background: url(/img/lion-128.png) no-repeat center/80% transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ z-index: 1;
+ pointer-events: all;
+ transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55),
+ background 0.25s ease,
+ box-shadow 0.25s ease;
+
+ &:hover {
+ bottom: -45px;
+ }
+
+ &-open, &-open:hover,
+ &-has-new, &-has-new:hover {
+ background-color: #FFF;
+ box-shadow: @announcements-shadow;
+ bottom: 30px;
+ }
+}
+ .announcements-badge {
+ display: inline-block;
+ position: absolute;
+ font-size: 11px;
+ height: 1.8em;
+ min-width: 1.8em;
+ border-radius: 0.9em;
+ line-height: 1.8;
+ padding: 0 2px;
+ top: 1px;
+ right: 1px;
+ font-weight: bold;
+ color: #FFF;
+ background-color: @red;
+ vertical-align: baseline;
+ white-space: nowrap;
+ text-align: center;
+ z-index: 1;
+ animation: pulse 1s alternate infinite;
+ }
+
+.announcements-body {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ position: absolute;
+ right: 3%;
+ margin-right: 95px;
+ bottom: 30px;
+ width: 700px;
+ max-height: 52%;
+ min-height: 100px;
+ background: #FFF;
+ z-index: 1;
+ box-shadow: @announcements-shadow;
+ border-radius: @border-radius-base;
+ animation: fade-in 0.35s forwards;
+
+ &::after {
+ content: "\25b8";
+ position: absolute;
+ left: 100%;
+ bottom: 17px;
+ width: 30px;
+ color: #FFF;
+ text-shadow: @announcements-shadow;
+ font-size: 2em;
+ overflow: hidden;
+ text-indent: -7px;
+ }
+}
+
+ .announcements-scroller {
+ padding: @line-height-computed;
+ flex-grow: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+ .announcement {
+ margin-bottom: @line-height-computed * 1.5;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ .announcement-header {
+ .page-header;
+ margin: 0;
+ }
+
+ .announcement-description {
+ margin: (@line-height-computed / 4) 0 (@line-height-computed / 2);
+ }
+
+ .announcement-meta {
+ .clearfix;
+ font-size: 0.9em;
+ }
+
+ .announcement-date {
+ float: left;
+ color: @gray;
+ margin: 0;
+ }
+
+ .announcement-link {
+ float: right;
+ margin: 0;
+ }
diff --git a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee
index 7578ea3a64..49e8292f97 100644
--- a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee
@@ -42,10 +42,13 @@ describe 'AnnouncementsHandler', ->
@BlogHandler.getLatestAnnouncements.callsArgWith(0, null, @stubbedAnnouncements)
- it "should return all announcements if there are no getLastOccurance", (done)->
+ it "should mark all announcements as read is false", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
- announcements.length.should.equal 4
+ announcements[0].read.should.equal false
+ announcements[1].read.should.equal false
+ announcements[2].read.should.equal false
+ announcements[3].read.should.equal false
done()
it "should should be sorted again to ensure correct order", (done)->
@@ -57,16 +60,30 @@ describe 'AnnouncementsHandler', ->
announcements[0].should.equal @stubbedAnnouncements[0]
done()
- it "should return ones older than the last blog id", (done)->
+ it "should return older ones marked as read as well", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2014/04/12/title-date-irrelivant"}})
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
- announcements.length.should.equal 2
announcements[0].id.should.equal @stubbedAnnouncements[0].id
+ announcements[0].read.should.equal false
+
announcements[1].id.should.equal @stubbedAnnouncements[1].id
+ announcements[1].read.should.equal false
+
+ announcements[2].id.should.equal @stubbedAnnouncements[3].id
+ announcements[2].read.should.equal true
+
+ announcements[3].id.should.equal @stubbedAnnouncements[2].id
+ announcements[3].read.should.equal true
+
done()
- it "should return none when the latest id is the first element", (done)->
+ it "should return all of them marked as read", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}})
@handler.getUnreadAnnouncements @user_id, (err, announcements)=>
- announcements.length.should.equal 0
+ announcements[0].read.should.equal true
+ announcements[1].read.should.equal true
+ announcements[2].read.should.equal true
+ announcements[3].read.should.equal true
done()
+
+
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 f01e2c7015..28bf1ab6a2 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -27,6 +27,7 @@ describe "CollaboratorsInviteController", ->
"../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
"../Analytics/AnalyticsManager": @AnalyticsManger
'../Authentication/AuthenticationController': @AuthenticationController
+ 'settings-sharelatex': @settings = {}
@res = new MockResponse()
@req = new MockRequest()
@@ -103,9 +104,15 @@ describe "CollaboratorsInviteController", ->
describe 'when all goes well', ->
beforeEach ->
+ @_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 produce json response', ->
@res.json.callCount.should.equal 1
({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0])
@@ -114,6 +121,10 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
+ it 'should have called _checkShouldInviteEmail', ->
+ @_checkShouldInviteEmail.callCount.should.equal 1
+ @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
@@ -125,37 +136,63 @@ describe "CollaboratorsInviteController", ->
describe 'when the user is not allowed to add more collaborators', ->
beforeEach ->
+ @_checkShouldInviteEmail = sinon.stub(
+ @CollaboratorsInviteController, '_checkShouldInviteEmail'
+ ).callsArgWith(1, null, true)
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
+ afterEach ->
+ @_checkShouldInviteEmail.restore()
+
it 'should produce json response without an invite', ->
@res.json.callCount.should.equal 1
({invite: null}).should.deep.equal(@res.json.firstCall.args[0])
+ it 'should not have called _checkShouldInviteEmail', ->
+ @_checkShouldInviteEmail.callCount.should.equal 0
+ @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
+
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when canAddXCollaborators produces an error', ->
beforeEach ->
+ @_checkShouldInviteEmail = sinon.stub(
+ @CollaboratorsInviteController, '_checkShouldInviteEmail'
+ ).callsArgWith(1, null, true)
@err = new Error('woops')
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
+ afterEach ->
+ @_checkShouldInviteEmail.restore()
+
it 'should call next with an error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
+ it 'should not have called _checkShouldInviteEmail', ->
+ @_checkShouldInviteEmail.callCount.should.equal 0
+ @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
+
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
describe 'when inviteToProject produces an error', ->
beforeEach ->
+ @_checkShouldInviteEmail = sinon.stub(
+ @CollaboratorsInviteController, '_checkShouldInviteEmail'
+ ).callsArgWith(1, null, true)
@err = new Error('woops')
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
+ afterEach ->
+ @_checkShouldInviteEmail.restore()
+
it 'should call next with an error', ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
@@ -164,10 +201,60 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
+ it 'should have called _checkShouldInviteEmail', ->
+ @_checkShouldInviteEmail.callCount.should.equal 1
+ @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
+ describe 'when _checkShouldInviteEmail disallows the invite', ->
+
+ beforeEach ->
+ @_checkShouldInviteEmail = sinon.stub(
+ @CollaboratorsInviteController, '_checkShouldInviteEmail'
+ ).callsArgWith(1, null, false)
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ afterEach ->
+ @_checkShouldInviteEmail.restore()
+
+ it 'should produce json response with no invite, and an error property', ->
+ @res.json.callCount.should.equal 1
+ ({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0])
+
+ it 'should have called _checkShouldInviteEmail', ->
+ @_checkShouldInviteEmail.callCount.should.equal 1
+ @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
+ it 'should not have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
+ describe 'when _checkShouldInviteEmail produces an error', ->
+
+ beforeEach ->
+ @_checkShouldInviteEmail = sinon.stub(
+ @CollaboratorsInviteController, '_checkShouldInviteEmail'
+ ).callsArgWith(1, new Error('woops'))
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ afterEach ->
+ @_checkShouldInviteEmail.restore()
+
+ it 'should call next with an error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should have called _checkShouldInviteEmail', ->
+ @_checkShouldInviteEmail.callCount.should.equal 1
+ @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
+ it 'should not have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
describe "viewInvite", ->
beforeEach ->
@@ -579,3 +666,74 @@ describe "CollaboratorsInviteController", ->
it 'should have called acceptInvite', ->
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
+
+ describe '_checkShouldInviteEmail', ->
+
+ beforeEach ->
+ @email = 'user@example.com'
+ @call = (callback) =>
+ @CollaboratorsInviteController._checkShouldInviteEmail @email, callback
+
+ describe 'when we should be restricting to existing accounts', ->
+
+ beforeEach ->
+ @settings.restrictInvitesToExistingAccounts = true
+
+ describe 'when user account is present', ->
+
+ beforeEach ->
+ @user = {_id: ObjectId().toString()}
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
+
+ it 'should callback with `true`', (done) ->
+ @call (err, shouldAllow) =>
+ expect(err).to.equal null
+ expect(shouldAllow).to.equal true
+ done()
+
+ describe 'when user account is absent', ->
+
+ beforeEach ->
+ @user = null
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
+
+ it 'should callback with `false`', (done) ->
+ @call (err, shouldAllow) =>
+ expect(err).to.equal null
+ expect(shouldAllow).to.equal false
+ done()
+
+ it 'should have called getUser', (done) ->
+ @call (err, shouldAllow) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @email}, {_id: 1}).should.equal true
+ done()
+
+ describe 'when getUser produces an error', ->
+
+ beforeEach ->
+ @user = null
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
+
+ it 'should callback with an error', (done) ->
+ @call (err, shouldAllow) =>
+ expect(err).to.not.equal null
+ expect(err).to.be.instanceof Error
+ expect(shouldAllow).to.equal undefined
+ done()
+
+ describe 'when we should not be restricting', ->
+
+ beforeEach ->
+ @settings.restrictInvitesToExistingAccounts = false
+
+ it 'should callback with `true`', (done) ->
+ @call (err, shouldAllow) =>
+ expect(err).to.equal null
+ expect(shouldAllow).to.equal true
+ done()
+
+ it 'should not have called getUser', (done) ->
+ @call (err, shouldAllow) =>
+ @UserGetter.getUser.callCount.should.equal 0
+ done()