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

Merge branch 'master' into ja-review-panel

Conflicts:
	app/views/project/editor/editor.jade
	public/coffee/ide.coffee
	public/coffee/ide/editor/directives/aceEditor.coffee
James Allen 10 лет назад
Родитель
Сommit
8368577867
100 измененных файлов с 1487 добавлено и 787 удалено
  1. 0 21
      services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
  2. 49 20
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  3. 1 0
      services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee
  4. 18 6
      services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
  5. 2 3
      services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
  6. 19 5
      services/web/app/coffee/Features/User/UserController.coffee
  7. 1 1
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  8. 4 0
      services/web/app/coffee/infrastructure/LoggerSerializers.coffee
  9. 1 1
      services/web/app/coffee/router.coffee
  10. 3 2
      services/web/app/views/layout.jade
  11. 3 0
      services/web/app/views/project/editor.jade
  12. 1 0
      services/web/app/views/project/editor/editor.jade
  13. 76 32
      services/web/app/views/project/editor/history.jade
  14. 6 5
      services/web/app/views/subscriptions/dashboard.jade
  15. 137 125
      services/web/app/views/subscriptions/plans.jade
  16. 1 1
      services/web/app/views/subscriptions/successful_subscription.jade
  17. 18 2
      services/web/app/views/user/settings.jade
  18. 1 1
      services/web/package.json
  19. 9 3
      services/web/public/coffee/ide.coffee
  20. 29 2
      services/web/public/coffee/ide/connection/ConnectionManager.coffee
  21. 3 1
      services/web/public/coffee/ide/editor/EditorManager.coffee
  22. 50 12
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  23. 9 1
      services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
  24. 4 4
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  25. 4 0
      services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
  26. 6 1
      services/web/public/coffee/ide/references/ReferencesManager.coffee
  27. 1 1
      services/web/public/coffee/ide/settings/controllers/SettingsController.coffee
  28. 21 5
      services/web/public/coffee/main/account-settings.coffee
  29. 29 9
      services/web/public/coffee/main/account-upgrade.coffee
  30. 5 2
      services/web/public/coffee/main/new-subscription.coffee
  31. 189 12
      services/web/public/coffee/main/plans.coffee
  32. 26 11
      services/web/public/coffee/main/subscription-dashboard.coffee
  33. BIN
      services/web/public/img/teasers/history/teaser-history.png
  34. 147 118
      services/web/public/js/ace-1.2.5/mode-latex.js
  35. 607 380
      services/web/public/js/ace-1.2.5/worker-latex.js
  36. 1 0
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/.gitattributes
  37. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-EUC-H.bcmap
  38. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-EUC-V.bcmap
  39. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-H.bcmap
  40. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-RKSJ-H.bcmap
  41. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-RKSJ-V.bcmap
  42. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-V.bcmap
  43. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78ms-RKSJ-H.bcmap
  44. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78ms-RKSJ-V.bcmap
  45. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/83pv-RKSJ-H.bcmap
  46. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90ms-RKSJ-H.bcmap
  47. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90ms-RKSJ-V.bcmap
  48. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90msp-RKSJ-H.bcmap
  49. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90msp-RKSJ-V.bcmap
  50. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90pv-RKSJ-H.bcmap
  51. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90pv-RKSJ-V.bcmap
  52. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-H.bcmap
  53. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-RKSJ-H.bcmap
  54. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-RKSJ-V.bcmap
  55. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-V.bcmap
  56. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-0.bcmap
  57. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-1.bcmap
  58. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-2.bcmap
  59. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-3.bcmap
  60. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-4.bcmap
  61. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-5.bcmap
  62. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-6.bcmap
  63. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-UCS2.bcmap
  64. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-0.bcmap
  65. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-1.bcmap
  66. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-2.bcmap
  67. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-3.bcmap
  68. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-4.bcmap
  69. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-5.bcmap
  70. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-UCS2.bcmap
  71. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-0.bcmap
  72. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-1.bcmap
  73. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-2.bcmap
  74. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-3.bcmap
  75. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-4.bcmap
  76. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-5.bcmap
  77. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-6.bcmap
  78. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-UCS2.bcmap
  79. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-0.bcmap
  80. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-1.bcmap
  81. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-2.bcmap
  82. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-UCS2.bcmap
  83. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5-H.bcmap
  84. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5-V.bcmap
  85. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5pc-H.bcmap
  86. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5pc-V.bcmap
  87. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS-EUC-H.bcmap
  88. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS-EUC-V.bcmap
  89. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS1-H.bcmap
  90. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS1-V.bcmap
  91. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS2-H.bcmap
  92. 3 0
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS2-V.bcmap
  93. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETHK-B5-H.bcmap
  94. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETHK-B5-V.bcmap
  95. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETen-B5-H.bcmap
  96. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETen-B5-V.bcmap
  97. 3 0
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETenms-B5-H.bcmap
  98. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETenms-B5-V.bcmap
  99. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/EUC-H.bcmap
  100. BIN
      services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/EUC-V.bcmap

+ 0 - 21
services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee

@@ -11,27 +11,6 @@ module.exports = CollaboratorsEmailHandler =
 			"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
 		].join("&")
 
-	notifyUserOfProjectShare: (project_id, email, callback)->
-		Project
-			.findOne(_id: project_id )
-			.select("name owner_ref")
-			.populate('owner_ref')
-			.exec (err, project)->
-				emailOptions =
-					to: email
-					replyTo: project.owner_ref.email
-					project:
-						name: project.name
-						url: "#{Settings.siteUrl}/project/#{project._id}?" + [
-								"project_name=#{encodeURIComponent(project.name)}"
-								"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
-								"new_email=#{encodeURIComponent(email)}"
-								"r=#{project.owner_ref.referal_id}" # Referal
-								"rs=ci" # referral source = collaborator invite
-							].join("&")
-					owner: project.owner_ref
-				EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
-
 	notifyUserOfProjectInvite: (project_id, email, invite, callback)->
 		Project
 			.findOne(_id: project_id )

+ 49 - 20
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -7,10 +7,18 @@ settings = require("settings-sharelatex")
 
 templates = {}
 
+
 templates.registered =
 	subject:  _.template "Activate your #{settings.appName} Account"
 	layout: PersonalEmailLayout
 	type: "notification"
+	plainTextTemplate: _.template """
+Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
+
+Click here to set your password and log in: <%= setNewPasswordUrl %>
+
+If you have any questions or problems, please contact #{settings.adminEmail}
+"""
 	compiledTemplate: _.template """
 <p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
 
@@ -19,10 +27,24 @@ templates.registered =
 <p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
 """
 
+
 templates.canceledSubscription =
 	subject:  _.template "ShareLaTeX thoughts"
 	layout: PersonalEmailLayout
 	type:"lifecycle"
+	plainTextTemplate: _.template """
+Hi <%= first_name %>,
+
+I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
+
+https://sharelatex.typeform.com/to/f5lBiZ
+
+Thank you in advance.
+
+Henry
+
+ShareLaTeX Co-founder
+"""
 	compiledTemplate: _.template '''
 <p>Hi <%= first_name %>,</p>
 
@@ -36,10 +58,26 @@ ShareLaTeX Co-founder
 </p>
 '''
 
+
 templates.passwordResetRequested =
 	subject:  _.template "Password Reset - #{settings.appName}"
 	layout: NotificationEmailLayout
 	type:"notification"
+	plainTextTemplate: _.template """
+Password Reset
+
+We got a request to reset your #{settings.appName} password.
+
+Click this link to reset your password: <%= setNewPasswordUrl %>
+
+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} - <%= siteUrl %>
+"""
 	compiledTemplate: _.template """
 <h2>Password Reset</h2>
 <p>
@@ -66,26 +104,6 @@ If you didn't request a password reset, let us know.
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 """
 
-templates.projectSharedWithYou =
-	subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
-	layout: NotificationEmailLayout
-	type:"notification"
-	compiledTemplate: _.template """
-<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
-<center>
-	<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
-		<div style="padding-right:10px;padding-left:10px">
-			<a href="<%= project.url %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
-		  			View Project
-				</span>
-			</a>
-		</div>
-	</div>
-</center>
-<p> Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
 
 templates.projectInvite =
 	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
@@ -113,10 +131,20 @@ Thank you
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 """
 
+
 templates.completeJoinGroupAccount =
 	subject: _.template "Verify Email to join <%= group_name %> group"
 	layout: NotificationEmailLayout
 	type:"notification"
+	plainTextTemplate: _.template """
+Hi, please verify your email to join the <%= group_name %> and get your free premium account
+
+Click this link to verify now: <%= completeJoinUrl %>
+
+Thank You
+
+#{settings.appName} - <%= siteUrl %>
+"""
 	compiledTemplate: _.template """
 <p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
 <center>
@@ -134,6 +162,7 @@ templates.completeJoinGroupAccount =
 <p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
 """
 
+
 module.exports =
 	templates: templates
 

+ 1 - 0
services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee

@@ -63,6 +63,7 @@ Reporter = (res) ->
 
 			res.contentType("application/json")
 			if failures.length > 0
+				logger.err failures:failures, "health check failed"
 				res.send 500, JSON.stringify(results, null, 2)
 			else
 				res.send 200, JSON.stringify(results, null, 2)

+ 18 - 6
services/web/app/coffee/Features/Subscription/SubscriptionController.coffee

@@ -8,6 +8,7 @@ Settings   = require 'settings-sharelatex'
 logger     = require('logger-sharelatex')
 GeoIpLookup = require("../../infrastructure/GeoIpLookup")
 SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
+UserGetter = require "../User/UserGetter"
 
 module.exports = SubscriptionController =
 
@@ -21,14 +22,25 @@ module.exports = SubscriptionController =
 		if req.query.v?
 			viewName = "#{viewName}_#{req.query.v}"
 		logger.log viewName:viewName, "showing plans page"
+		currentUser = null
 		GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
 			return next(err) if err?
-			res.render viewName,
-				title: "plans_and_pricing"
-				plans: plans
-				baseUrl: baseUrl
-				gaExperiments: Settings.gaExperiments.plansPage
-				recomendedCurrency:recomendedCurrency
+			render = () ->
+				res.render viewName,
+					title: "plans_and_pricing"
+					plans: plans
+					baseUrl: baseUrl
+					gaExperiments: Settings.gaExperiments.plansPage
+					recomendedCurrency:recomendedCurrency
+					shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
+			user_id = AuthenticationController.getLoggedInUserId(req)
+			if user_id?
+				UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
+					return next(err) if err?
+					currentUser = user
+					render()
+			else
+				render()
 
 	#get to show the recurly.js page
 	paymentPage: (req, res, next) ->

+ 2 - 3
services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee

@@ -44,7 +44,7 @@ module.exports =
 		allPlans = {}
 		plans.forEach (plan)->
 			allPlans[plan.planCode] = plan
-			
+
 		result =
 			allPlans: allPlans
 
@@ -54,7 +54,7 @@ module.exports =
 
 		result.studentAccounts = _.filter plans, (plan)->
 			plan.planCode.indexOf("student") != -1
-			
+
 		result.groupMonthlyPlans = _.filter plans, (plan)->
 			plan.groupPlan and !plan.annual
 
@@ -68,4 +68,3 @@ module.exports =
 			!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
 
 		return result
-

+ 19 - 5
services/web/app/coffee/Features/User/UserController.coffee

@@ -15,12 +15,26 @@ settings = require "settings-sharelatex"
 
 module.exports = UserController =
 
-	deleteUser: (req, res)->
+	tryDeleteUser: (req, res, next) ->
 		user_id = AuthenticationController.getLoggedInUserId(req)
-		UserDeleter.deleteUser user_id, (err)->
-			if !err?
+		password = req.body.password
+		logger.log {user_id}, "trying to delete user account"
+		if !password? or password == ''
+			logger.err {user_id}, 'no password supplied for attempt to delete account'
+			return res.sendStatus(403)
+		AuthenticationManager.authenticate {_id: user_id}, password, (err, user) ->
+			if err?
+				logger.err {user_id}, 'error authenticating during attempt to delete account'
+				return next(err)
+			if !user
+				logger.err {user_id}, 'auth failed during attempt to delete account'
+				return res.sendStatus(403)
+			UserDeleter.deleteUser user_id, (err) ->
+				if err?
+					logger.err {user_id}, "error while deleting user account"
+					return next(err)
 				req.session?.destroy()
-			res.sendStatus(200)
+				res.sendStatus(200)
 
 	unsubscribe: (req, res)->
 		user_id = AuthenticationController.getLoggedInUserId(req)
@@ -143,7 +157,7 @@ module.exports = UserController =
 									type:'success'
 									text:'Your password has been changed'
 			else
-				logger.log user: user, "current password wrong"
+				logger.log user_id: user_id, "current password wrong"
 				res.send
 					message:
 					  type:'error'

+ 1 - 1
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -39,7 +39,7 @@ pathList = [
 	["#{jsPath}ide.js"]
 	["#{jsPath}main.js"]
 	["#{jsPath}libs.js"]
-	["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js", "#{jsPath}#{ace}/snippets/latex.js"]
+	["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"]
 	["#{jsPath}libs/#{pdfjs}/pdf.js"]
 	["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
 	["#{jsPath}libs/#{pdfjs}/compatibility.js"]

+ 4 - 0
services/web/app/coffee/infrastructure/LoggerSerializers.coffee

@@ -1,5 +1,7 @@
 module.exports =
 	user: (user) ->
+		if !user?
+			return null
 		if !user._id?
 			user = {_id : user}
 		return {
@@ -10,6 +12,8 @@ module.exports =
 		}
 	
 	project: (project) ->
+		if !project?
+			return null
 		if !project._id?
 			project = {_id: project}
 		return {

+ 1 - 1
services/web/app/coffee/router.coffee

@@ -92,7 +92,7 @@ module.exports = class Router
 		webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
 
 		webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
-		webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
+		webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser
 
 		webRouter.get  '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
 		apiRouter.get  '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo

+ 3 - 2
services/web/app/views/layout.jade

@@ -51,8 +51,6 @@ html(itemscope, itemtype='http://schema.org/Product')
 		script(type="text/javascript").
 			window.csrfToken = "#{csrfToken}";
 			
-		block scripts
-
 		script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
 		script(type="text/javascript").
 			var noCdnKey = "nocdn=true"
@@ -61,6 +59,9 @@ html(itemscope, itemtype='http://schema.org/Product')
 			if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
 				window.location.search += '&'+noCdnKey;
 			}
+
+		block scripts
+
 		script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
 
 		script.

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

@@ -118,6 +118,9 @@ block requirejs
 				"ace/ext-searchbox": {
 					"deps": ["ace/ace"]
 				},
+				"ace/ext-modelist": {
+					"deps": ["ace/ace"]
+				},
 				"ace/ext-language_tools": {
 					"deps": ["ace/ace"]
 				}

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

@@ -35,6 +35,7 @@ div.full-size(
 			resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,reviewPanel:toggle",
 			annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
 			read-only="!permissions.write",
+			file-name="editor.open_doc_name",
 			on-ctrl-enter="recompileViaKey",
 			syntax-validation="settings.syntaxValidation",
 			review-panel="reviewPanel",

+ 76 - 32
services/web/app/views/project/editor/history.jade

@@ -1,41 +1,85 @@
 div#history(ng-show="ui.view == 'history'")
 	span(ng-controller="HistoryPremiumPopup")
-		.upgrade-prompt(ng-show="!project.features.versioning")
-			.message(ng-show="project.owner._id == user.id")
-				p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
-				p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
-				ul.list-unstyled
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("unlimited_projects")}
-					
-					li
-						i.fa.fa-check &nbsp;
-						| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
-					
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("full_doc_history")}
-					
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("sync_to_dropbox")}
+		.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
+			
+			div(sixpack-switch="teaser-history")
+				.message(
+					sixpack-default
+					ng-show="project.owner._id == user.id"
+				)
+					p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
+					p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+					ul.list-unstyled
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("unlimited_projects")}
+						
+						li
+							i.fa.fa-check &nbsp;
+							| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
+						
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("full_doc_history")}
+						
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("sync_to_dropbox")}
 
-					li 
-						i.fa.fa-check &nbsp;
-						| #{translate("sync_to_github")}
+						li 
+							i.fa.fa-check &nbsp;
+							| #{translate("sync_to_github")}
 
-					li  
-						i.fa.fa-check &nbsp;
-						|#{translate("compile_larger_projects")}
+						li  
+							i.fa.fa-check &nbsp;
+							|#{translate("compile_larger_projects")}
+					p.text-center(ng-controller="FreeTrialModalController")
+						a.btn.btn-success(
+							href
+							ng-class="buttonClass"
+							ng-click="startFreeTrial('history')"
+							sixpack-convert="teaser-history"
+						) #{translate("start_free_trial")}
 
-				p.text-center(ng-controller="FreeTrialModalController")
-					a.btn.btn-success(
-						href
-						ng-class="buttonClass"
-						ng-click="startFreeTrial('history')"
-					) #{translate("start_free_trial")}
+				.message.message-wider(
+					sixpack-when="focused"
+					ng-show="project.owner._id == user.id"
+				)
+					header.message-header
+						h3 History
 
+					.message-body
+						h4.teaser-title See who changed what. Go back to previous versions.
+						img.teaser-img(
+							src="/img/teasers/history/teaser-history.png"
+							alt="History"
+						)
+						p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+						.row
+							.col-md-8.col-md-offset-2
+								ul.list-unstyled
+									li 
+										i.fa.fa-check &nbsp;
+										| Catch up with your collaborators changes
+									
+									li
+										i.fa.fa-check &nbsp;
+										| See changes over any time period
+									
+									li 
+										i.fa.fa-check &nbsp;
+										| Revert your documents to previous versions
+									
+									li 
+										i.fa.fa-check &nbsp;
+										| Restore deleted files
+						p.text-center(ng-controller="FreeTrialModalController")
+							a.btn.btn-success(
+								href
+								ng-class="buttonClass"
+								ng-click="startFreeTrial('history')"
+								sixpack-convert="teaser-history"
+							) Try it for free
 
 			.message(ng-show="project.owner._id != user.id")
 				p #{translate("ask_proj_owner_to_upgrade_for_history")}

+ 6 - 5
services/web/app/views/subscriptions/dashboard.jade

@@ -12,8 +12,8 @@ block scripts
 
 mixin printPlan(plan)
 	-if (!plan.hideFromUsers)
-		tr(ng-controller="ChangePlanFormController")
-			td(ng-init="plan=#{JSON.stringify(plan)}")
+		tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)")
+			td
 				strong #{plan.name}
 			td {{refreshPrice(plan.planCode)}}
 				-if (plan.annual)
@@ -46,8 +46,8 @@ block content
 							| &nbsp;
 							| #{translate("your_billing_details_were_saved")}
 					.card(ng-if="view == 'overview'")
-						.page-header
-							h1 #{translate("your_subscription")}
+						.page-header(x-current-plan="#{subscription.planCode}")
+							h1 #{translate("your_subscription")} 
 
 						- if (subscription && user._id+'' == subscription.admin_id+'')
 							case subscription.state
@@ -56,7 +56,8 @@ block content
 									
 								when "active"
 									p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})} 
-										a(href, ng-click="changePlan = true") !{translate("change_plan")}.
+										span(ng-show="!isNextGenPlan")
+											a(href, ng-click="changePlan = true") !{translate("change_plan")}.
 									p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
 									p.pull-right
 									p

+ 137 - 125
services/web/app/views/subscriptions/plans.jade

@@ -3,6 +3,7 @@ block scripts
 	script(type='text/javascript').
 		window.recomendedCurrency = '#{recomendedCurrency}'
 		window.abCurrencyFlag = '#{abCurrencyFlag}'
+		window.shouldABTestPlans = #{shouldABTestPlans || false}
 
 	script(type='text/javascript').
 		(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; 
@@ -56,133 +57,144 @@ block content
 										ng-click="changeCurreny(currency)"
 									) {{currency}} ({{value['symbol']}})
 
-				.row(ng-cloak)
-					.col-md-10.col-md-offset-1
-						.row
-							.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
-								.col-md-4
-									.card.card-first
-										.card-header
-											h2 #{translate("personal")} 
-										.circle #{translate("free")} 
-										ul.list-unstyled
-											li #{translate("one_collaborator")}
-											li &nbsp;
-											li &nbsp;
-											li &nbsp;
-											li
-												br
-												a.btn.btn-info(
-													href="/register"
-													style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
-												) #{translate("sign_up_now")}
-								.col-md-4
-									.card.card-highlighted
-										.card-header
-											h2 #{translate("collaborator")}
-										.circle
-											span(ng-if="ui.view == 'monthly'")
-												| {{plans[currencyCode]['collaborator']['monthly']}}
-												span.small /mo
-											span(ng-if="ui.view == 'annual'")
-												| {{plans[currencyCode]['collaborator']['annual']}}
-												span.small /yr
-										ul.list-unstyled
-											li 
-												strong #{translate("collabs_per_proj", {collabcount:10})}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
+				div(ng-show="showPlans")
+					.row(ng-cloak)
+						.col-md-10.col-md-offset-1
+							.row
+								.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
+									.col-md-4
+										.card.card-first
+											.card-header
+												h2 #{translate("personal")} 
+											.circle #{translate("free")} 
+											ul.list-unstyled
+												li #{translate("one_collaborator")}
+												li &nbsp;
+												li &nbsp;
+												li &nbsp;
+												li
+													br
+													a.btn.btn-info(
+														href="/register"
+														style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+													) #{translate("sign_up_now")}
+									.col-md-4
+										.card.card-highlighted
+											.card-header
+												h2 #{translate("collaborator")}
+											.circle
+												span(ng-if="ui.view == 'monthly'")
+													| {{plans[currencyCode]['collaborator']['monthly']}}
+													span.small /mo
+												span(ng-if="ui.view == 'annual'")
+													| {{plans[currencyCode]['collaborator']['annual']}}
+													span.small /yr
+											ul.list-unstyled
+												li 
+													strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})}
+													strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})}
+													strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+
+														ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ (ui.view == 'annual' ? '-annual' : '') + (plansVariant == 'default' ? planQueryString : '_'+plansVariant)}}&currency={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
+													)
+														span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+														span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+									.col-md-4
+										.card.card-last
+											.card-header
+												h2 #{translate("professional")}
+											.circle 
+												span(ng-if="ui.view == 'monthly'")
+													| {{plans[currencyCode]['professional']['monthly']}}
+													span.small /mo
+												span(ng-if="ui.view == 'annual'")
+													| {{plans[currencyCode]['professional']['annual']}}
+													span.small /yr
+											ul.list-unstyled
+												li
+													strong #{translate("unlimited_collabs")}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+														ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('professional')"
+													)
+														span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+														span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+
+								.card-group.text-centered(ng-if="ui.view == 'student'")
+									.col-md-4
+										.card.card-first
+											.card-header
+												h2 #{translate("personal")}
+											.circle #{translate("free")}
+											ul.list-unstyled
+												li #{translate("one_collaborator")}
+												li &nbsp;
+												li &nbsp;
+												li &nbsp;
+												li
+													br
+													a.btn.btn-info(
+														href="/register"
+														style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+													) #{translate("sign_up_now")}
+
+									.col-md-4
+										.card.card-highlighted
+											.card-header
+												h2 #{translate("student")}
+											.circle
+												span
+													| {{plans[currencyCode]['student']['monthly']}}
+													span.small /mo
+											ul.list-unstyled
+												li
+													strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
+													strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
+													strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+														ng-href="#{baseUrl}/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}&currency={{currencyCode}}",
+														ng-click="signUpNowClicked('student')"
+													) #{translate("start_free_trial")}
+
+									.col-md-4
+										.card.card-last
+											.card-header
+												h2 #{translate("student")} (#{translate("annual")})
+											.circle
+												span
+													| {{plans[currencyCode]['student']['annual']}}
+													span.small /yr
+											ul.list-unstyled
+												li
+													strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
+													strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
+													strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
+												li #{translate("full_doc_history")}
+												li #{translate("sync_to_dropbox")}
+												li #{translate("sync_to_github")}
+												li
+													br
+													a.btn.btn-info(
+														ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}&currency={{currencyCode}}",
+														ng-click="signUpNowClicked('student')"
+													) #{translate("buy_now")}
 
-													ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
-												)
-													span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
-													span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
-								.col-md-4
-									.card.card-last
-										.card-header
-											h2 #{translate("professional")}
-										.circle 
-											span(ng-if="ui.view == 'monthly'")
-												| {{plans[currencyCode]['professional']['monthly']}}
-												span.small /mo
-											span(ng-if="ui.view == 'annual'")
-												| {{plans[currencyCode]['professional']['annual']}}
-												span.small /yr
-										ul.list-unstyled
-											li
-												strong #{translate("unlimited_collabs")}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
-													ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('professional')"
-												)
-													span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
-													span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
 
-							.card-group.text-centered(ng-if="ui.view == 'student'")
-								.col-md-4
-									.card.card-first
-										.card-header
-											h2 #{translate("personal")}
-										.circle #{translate("free")}
-										ul.list-unstyled
-											li #{translate("one_collaborator")}
-											li &nbsp;
-											li &nbsp;
-											li &nbsp;
-											li
-												br
-												a.btn.btn-info(
-													href="/register"
-													style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
-												) #{translate("sign_up_now")}
-								
-								.col-md-4
-									.card.card-highlighted
-										.card-header
-											h2 #{translate("student")}
-										.circle
-											span
-												| {{plans[currencyCode]['student']['monthly']}}
-												span.small /mo
-										ul.list-unstyled
-											li
-												strong #{translate("collabs_per_proj", {collabcount:6})}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
-													ng-href="#{baseUrl}/user/subscription/new?planCode=student{{planQueryString}}&currency={{currencyCode}}", ng-click="signUpNowClicked('student')"
-												) #{translate("start_free_trial")}
-								
-								.col-md-4
-									.card.card-last
-										.card-header
-											h2 #{translate("student")} (#{translate("annual")})
-										.circle
-											span
-												| {{plans[currencyCode]['student']['annual']}}
-												span.small /yr
-										ul.list-unstyled
-											li
-												strong #{translate("collabs_per_proj", {collabcount:6})}
-											li #{translate("full_doc_history")}
-											li #{translate("sync_to_dropbox")}
-											li #{translate("sync_to_github")}
-											li
-												br
-												a.btn.btn-info(
-													ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual&currency={{currencyCode}}", ng-click="signUpNowClicked('student')"
-												) #{translate("buy_now")}
 
 				.row.row-spaced(ng-cloak)
 					p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}

+ 1 - 1
services/web/app/views/subscriptions/successful_subscription.jade

@@ -2,7 +2,7 @@ extends ../layout
 
 block content
 	.content.content-alt
-		.container
+		.container(ng-controller="SuccessfulSubscriptionController")
 			.row
 				.col-md-8.col-md-offset-2
 					.card(ng-cloak)

+ 18 - 2
services/web/app/views/user/settings.jade

@@ -150,16 +150,32 @@ block content
 	script(type='text/ng-template', id='deleteAccountModalTemplate')
 		.modal-header
 			h3 #{translate("delete_account")}
-		.modal-body
-			p !{translate("delete_account_warning_message_2")}
+		div.modal-body#delete-account-modal
+			p !{translate("delete_account_warning_message_3")}
 			form(novalidate, name="deleteAccountForm")
+				label #{translate('email')}
 				input.form-control(
 					type="text",
+					autocomplete="off",
 					placeholder="",
 					ng-model="state.deleteText",
 					focus-on="open",
 					ng-keyup="checkValidation()"
 				)
+				label #{translate('password')}
+				input.form-control(
+					type="password",
+					autocomplete="off",
+					placeholder="",
+					ng-model="state.password",
+					ng-keyup="checkValidation()"
+				)
+			div(ng-if="state.error")
+				div.alert.alert-danger
+					| #{translate('generic_something_went_wrong')}
+			div(ng-if="state.invalidCredentials")
+				div.alert.alert-danger
+					| #{translate('email_or_password_wrong_try_again')}
 		.modal-footer
 			button.btn.btn-default(
 				ng-click="cancel()"

+ 1 - 1
services/web/package.json

@@ -35,7 +35,7 @@
     "lynx": "0.1.1",
     "marked": "^0.3.5",
     "method-override": "^2.3.3",
-    "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
+    "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.6.0",
     "mimelib": "0.2.14",
     "mocha": "1.17.1",
     "mongojs": "0.18.2",

+ 9 - 3
services/web/public/coffee/ide.coffee

@@ -69,6 +69,12 @@ define [
 			reviewPanelOpen: false
 		}
 		$scope.user = window.user
+
+
+		$scope.shouldABTestPlans = false
+		if $scope.user.signUpDate >= '2016-10-27'
+			$scope.shouldABTestPlans = true
+
 		$scope.settings = window.userSettings
 		$scope.anonymous = window.anonymous
 
@@ -81,7 +87,7 @@ define [
 		# Only run the header AB test for newly registered users.
 		_abTestStartDate = new Date(Date.UTC(2016, 8, 28))
 		_userSignUpDate = new Date(window.user.signUpDate)
-		
+
 		$scope.shouldABTestHeaderLabels = _userSignUpDate > _abTestStartDate
 		$scope.headerLabelsABVariant = ""
 
@@ -98,7 +104,7 @@ define [
 		# Tracking code.
 		$scope.$watch "ui.view", (newView, oldView) ->
 			if newView? and newView != "editor" and newView != "pdf"
-				event_tracking.sendMBOnce "ide-open-view-#{ newView }-once" 
+				event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
 
 		$scope.$watch "ui.chatOpen", (isOpen) ->
 			event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
@@ -111,7 +117,7 @@ define [
 		# End of tracking code.
 
 		window._ide = ide
-		
+
 		ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and /
 
 		ide.project_id = $scope.project_id = window.project_id

+ 29 - 2
services/web/public/coffee/ide/connection/ConnectionManager.coffee

@@ -27,6 +27,7 @@ define [], () ->
 
 			@connected = false
 			@userIsInactive = false
+			@gracefullyReconnecting = false
 			
 			@$scope.connection = 
 				reconnecting: false
@@ -54,6 +55,7 @@ define [], () ->
 			@ide.socket.on "connect", () =>
 				sl_console.log "[socket.io connect] Connected"
 				@connected = true
+				@gracefullyReconnecting = false
 				@ide.pushEvent("connected")
 
 				@$scope.$apply () =>
@@ -81,7 +83,7 @@ define [], () ->
 				@$scope.$apply () =>
 					@$scope.connection.reconnecting = false
 
-				if !$scope.connection.forced_disconnect and !@userIsInactive
+				if !$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
 					@startAutoReconnectCountdown()
 
 			@ide.socket.on 'forceDisconnect', (message) =>
@@ -97,7 +99,11 @@ define [], () ->
 				setTimeout () ->
 					location.reload()
 				, 10 * 1000
-				
+			
+			@ide.socket.on "reconnectGracefully", () =>
+				sl_console.log "Reconnect gracefully"
+				@reconnectGracefully()
+
 		joinProject: () ->
 			sl_console.log "[joinProject] joining..."
 			@ide.socket.emit 'joinProject', {
@@ -180,3 +186,24 @@ define [], () ->
 				@$scope.$apply () =>
 					@$scope.connection.inactive_disconnect = true
 
+		RECONNECT_GRACEFULLY_RETRY_INTERVAL: 5000 # ms
+		MAX_RECONNECT_GRACEFULLY_INTERVAL: 60 * 5 * 1000 # 5 minutes
+		reconnectGracefully: () ->
+			@reconnectGracefullyStarted ?= new Date()
+			userIsInactive = (new Date() - @lastUserAction) > @RECONNECT_GRACEFULLY_RETRY_INTERVAL
+			maxIntervalReached = (new Date() - @reconnectGracefullyStarted) > @MAX_RECONNECT_GRACEFULLY_INTERVAL
+			if userIsInactive or maxIntervalReached
+				sl_console.log "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
+				@_reconnectGracefullyNow()
+			else
+				sl_console.log "[reconnectGracefully] User is working, will try again in 5 seconds"
+				setTimeout () =>
+					@reconnectGracefully()
+				, @RECONNECT_GRACEFULLY_RETRY_INTERVAL
+		
+		_reconnectGracefullyNow: () ->
+			@gracefullyReconnecting = true
+			@reconnectGracefullyStarted = null
+			# Clear cookie so we don't go to the same backend server
+			$.cookie("SERVERID", "", { expires: -1, path: "/" })
+			@reconnectImmediately()

+ 3 - 1
services/web/public/coffee/ide/editor/EditorManager.coffee

@@ -8,11 +8,12 @@ define [
 			@$scope.editor = {
 				sharejs_doc: null
 				open_doc_id: null
+				open_doc_name: null
 				opening: true
 			}
 
 			@$scope.$on "entity:selected", (event, entity) =>
-				if (@$scope.ui.view != "track-changes" and entity.type == "doc")
+				if (@$scope.ui.view != "history" and entity.type == "doc")
 					@openDoc(entity)
 
 			@$scope.$on "entity:deleted", (event, entity) =>
@@ -59,6 +60,7 @@ define [
 				return
 
 			@$scope.editor.open_doc_id = doc.id
+			@$scope.editor.open_doc_name = doc.name
 
 			@ide.localStorage "doc.open_id.#{@$scope.project_id}", doc.id
 			@ide.fileTreeManager.selectEntity(doc)

+ 50 - 12
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -2,20 +2,26 @@ define [
 	"base"
 	"ace/ace"
 	"ace/ext-searchbox"
+	"ace/ext-modelist"
 	"ide/editor/directives/aceEditor/undo/UndoManager"
 	"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
 	"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
 	"ide/editor/directives/aceEditor/highlights/HighlightsManager"
 	"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
 	"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
-], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) ->
+], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) ->
 	EditSession = ace.require('ace/edit_session').EditSession
+	ModeList = ace.require('ace/ext/modelist')
 
 	# set the path for ace workers if using a CDN (from editor.jade)
 	if window.aceWorkerPath != ""
+		syntaxValidationEnabled = true
 		ace.config.set('workerPath', "#{window.aceWorkerPath}")
 	else
-		ace.config.setDefaultValue("session", "useWorker", false)
+		syntaxValidationEnabled = false
+
+	# By default, don't use workers - enable them per-session as required
+	ace.config.setDefaultValue("session", "useWorker", false)
 
 	# Ace loads its script itself, so we need to hook in to be able to clear
 	# the cache.
@@ -43,6 +49,7 @@ define [
 				readOnly: "="
 				annotations: "="
 				navigateHighlights: "="
+				fileName: "="
 				onCtrlEnter: "="
 				syntaxValidation: "="
 				reviewPanel: "="
@@ -62,6 +69,11 @@ define [
 
 				editor = ace.edit(element.find(".ace-editor-body")[0])
 				editor.$blockScrolling = Infinity
+
+				# disable auto insertion of brackets and quotes
+				editor.setOption('behavioursEnabled', false)
+				editor.setOption('wrapBehavioursEnabled', false)
+
 				window.editors ||= []
 				window.editors.push editor
 
@@ -198,16 +210,15 @@ define [
 					editor.setReadOnly !!value
 
 				scope.$watch "syntaxValidation", (value) ->
-					session = editor.getSession()
-					session.setOption("useWorker", value);
+					# ignore undefined settings here
+					# only instances of ace with an explicit value should set useWorker
+					# the history instance will have syntaxValidation undefined
+					if value? and syntaxValidationEnabled
+						session = editor.getSession()
+						session.setOption("useWorker", value);
 
 				editor.setOption("scrollPastEnd", true)
 
-				resetSession = () ->
-					session = editor.getSession()
-					session.setUseWrapMode(true)
-					session.setMode("ace/mode/latex")
-
 				updateCount = 0
 				onChange = () ->
 					updateCount++
@@ -229,9 +240,36 @@ define [
 					session = editor.getSession()
 					if session?
 						session.destroy()
-					editor.setSession(new EditSession(lines, "ace/mode/latex"))
-					resetSession()
-					session = editor.getSession()
+
+					# see if we can lookup a suitable mode from ace
+					# but fall back to text by default
+					try
+						if scope.fileName.match(/\.(Rtex|bbl)$/i)
+							# recognise Rtex and bbl as latex
+							mode = "ace/mode/latex"
+						else if scope.fileName.match(/\.(sty|cls|clo)$/)
+							# recognise some common files as tex
+							mode = "ace/mode/tex"
+						else
+							mode = ModeList.getModeForPath(scope.fileName).mode
+							# we prefer plain_text mode over text mode because ace's
+							# text mode is actually for code and has unwanted
+							# indenting (see wrapMethod in ace edit_session.js)
+							if mode is "ace/mode/text"
+								mode = "ace/mode/plain_text"
+					catch
+						mode = "ace/mode/plain_text"
+
+					# create our new session
+					session = new EditSession(lines, mode)
+
+					session.setUseWrapMode(true)
+					# use syntax validation only when explicitly set
+					if scope.syntaxValidation? and syntaxValidationEnabled
+						session.setOption("useWorker", scope.syntaxValidation);
+
+					# now attach session to editor
+					editor.setSession(session)
 
 					doc = session.getDocument()
 					doc.on "change", onChange

+ 9 - 1
services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee

@@ -233,8 +233,16 @@ define [
 			start = aceDelta.start
 			if !start?
 				error = new Error("aceDelta had no start event.")
+				JSONstringifyWithCycles = (o) ->
+					seen = []
+					return JSON.stringify o, (k,v) ->
+						if (typeof v == 'object')
+							if ( !seen.indexOf(v) )
+								return '__cycle__'
+							seen.push(v);
+						return v
 				Raven?.captureException(error, {
-					aceDelta: JSON.stringify(aceDelta)
+					aceDelta: JSONstringifyWithCycles(aceDelta)
 				})
 				throw error
 			linesBefore = docLines.slice(0, start.row)

+ 4 - 4
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -29,12 +29,12 @@ define [
 
 		$scope.$watch "shouldShowLogs", (shouldShow) ->
 			if shouldShow
-				$scope.$applyAsync () -> 
+				$scope.$applyAsync () ->
 					$scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65
 
 		# log hints tracking
 		$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
-		
+
 		$scope.trackLogHintsLearnMore = () ->
 			event_tracking.sendMB "logs-hints-learn-more"
 
@@ -108,7 +108,7 @@ define [
 				_csrf: window.csrfToken
 			}, {params: params}
 
-		parseCompileResponse = (response) ->		
+		parseCompileResponse = (response) ->
 
 			# keep last url
 			last_pdf_url = $scope.pdf.url
@@ -469,7 +469,7 @@ define [
 
 			event_tracking.sendMB "subscription-start-trial", { source }
 
-			window.open("/user/subscription/new?planCode=student_free_trial_7_days")
+			window.open("/user/subscription/new?planCode=#{$scope.startTrialPlanCode}")
 			$scope.startedFreeTrial = true
 
 	App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->

+ 4 - 0
services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee

@@ -30,6 +30,10 @@ define [
 			TEXTLAYER_TIMEOUT: 100
 
 			constructor: (@url, @options) ->
+				# set up external character mappings - needed for Japanese etc
+				window.PDFJS.cMapUrl = './bcmaps/'
+				window.PDFJS.cMapPacked = true
+
 				if window.location?.search?.indexOf("disable-font-face=true") >= 0
 					window.PDFJS.disableFontFace = true
 				else

+ 6 - 1
services/web/public/coffee/ide/references/ReferencesManager.coffee

@@ -17,8 +17,13 @@ define [
 			# When we join the project:
 			#   index all references files
 			#   and don't broadcast to all clients
+			@inited = false
 			@$scope.$on 'project:joined', (e) =>
-				@indexAllReferences(false)
+				# We only need to grab the references when the editor first loads,
+				# not on every reconnect
+				if !@inited
+					@inited = true
+					@indexAllReferences(false)
 
 			setTimeout(
 				(self) ->

+ 1 - 1
services/web/public/coffee/ide/settings/controllers/SettingsController.coffee

@@ -30,7 +30,7 @@ define [
 
 		$scope.$watch "settings.syntaxValidation", (syntaxValidation, oldSyntaxValidation) =>
 			if syntaxValidation != oldSyntaxValidation
-				settings.saveProjectSettings({syntaxValidation: syntaxValidation})
+				settings.saveSettings({syntaxValidation: syntaxValidation})
 
 		$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
 			return if @ignoreUpdates

+ 21 - 5
services/web/public/coffee/main/account-settings.coffee

@@ -29,10 +29,13 @@ define [
 	App.controller "DeleteAccountModalController", [
 		"$scope", "$modalInstance", "$timeout", "$http",
 		($scope,   $modalInstance,   $timeout,   $http) ->
-			$scope.state = 
+			$scope.state =
 				isValid : false
 				deleteText: ""
+				password: ""
 				inflight: false
+				error: false
+				invalidCredentials: false
 
 			$modalInstance.opened.then () ->
 				$timeout () ->
@@ -40,20 +43,33 @@ define [
 				, 700
 
 			$scope.checkValidation = ->
-				$scope.state.isValid = $scope.state.deleteText == $scope.email
+				$scope.state.isValid = $scope.state.deleteText == $scope.email and $scope.state.password.length > 0
 
 			$scope.delete = () ->
 				$scope.state.inflight = true
-
+				$scope.state.error = false
+				$scope.state.invalidCredentials = false
 				$http({
-						method: "DELETE"
-						url: "/user"
+						method: "POST"
+						url: "/user/delete"
 						headers:
 							"X-CSRF-Token": window.csrfToken
+							"Content-Type": 'application/json'
+						data:
+							password: $scope.state.password
 					})
 					.success () ->
 						$modalInstance.close()
+						$scope.state.inflight = false
+						$scope.state.error = false
+						$scope.state.invalidCredentials = false
 						window.location = "/"
+					.error (data, status) ->
+						$scope.state.inflight = false
+						if status == 403
+							$scope.state.invalidCredentials = true
+						else
+							$scope.state.error = true
 
 			$scope.cancel = () ->
 				$modalInstance.dismiss('cancel')

+ 29 - 9
services/web/public/coffee/main/account-upgrade.coffee

@@ -6,14 +6,34 @@ define [
 		$scope.buttonClass = "btn-primary"
 
 		$scope.startFreeTrial = (source, couponCode) ->
-			event_tracking.sendMB "subscription-start-trial", { source }
+			plan = 'collaborator_free_trial_7_days'
 
 			w = window.open()
-			sixpack.convert "track-changes-discount", ->
-				sixpack.participate 'in-editor-free-trial-plan', ['student', 'collaborator'], (planName, rawResponse)->
-					ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
-					url = "/user/subscription/new?planCode=#{planName}_free_trial_7_days&ssp=#{planName == 'collaborator'}"
-					if couponCode?
-						url = "#{url}&cc=#{couponCode}"
-					$scope.startedFreeTrial = true
-					w.location = url
+			go = () ->
+				ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
+				url = "/user/subscription/new?planCode=#{plan}&ssp=true"
+				if couponCode?
+					url = "#{url}&cc=#{couponCode}"
+				$scope.startedFreeTrial = true
+
+				switch source
+					when "dropbox"
+						sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) ->
+							event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
+
+					when "history"
+						sixpack.participate 'teaser-history', ['default', 'focused'], (variant) ->
+							event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
+
+					else
+						event_tracking.sendMB "subscription-start-trial", { source, plan }
+				
+				w.location = url
+
+			if $scope.shouldABTestPlans
+				sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
+					if chosenVariation in ['heron', 'ibis']
+						plan = "collaborator_#{chosenVariation}"
+					go()
+			else
+				go()

+ 5 - 2
services/web/public/coffee/main/new-subscription.coffee

@@ -5,12 +5,15 @@ define [
 
 	App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
 		throw new Error("Recurly API Library Missing.")  if typeof recurly is "undefined"
-	
+
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 		$scope.plans = MultiCurrencyPricing.plans
 
 		$scope.switchToStudent = ()->
-			window.location = "/user/subscription/new?planCode=student_free_trial_7_days&currency=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
+			currentPlanCode = window.plan_code
+			planCode = currentPlanCode.replace('collaborator', 'student')
+			event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code }
+			window.location = "/user/subscription/new?planCode=#{planCode}&currency=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
 
 		event_tracking.sendMB "subscription-form", { plan : window.plan_code }
 

+ 189 - 12
services/web/public/coffee/main/plans.coffee

@@ -5,12 +5,171 @@ define [
 
 
 	App.factory "MultiCurrencyPricing", () ->
-		
+
 		currencyCode = window.recomendedCurrency
 
 		return {
 			currencyCode:currencyCode
-			plans: 
+
+			heron:
+				USD:
+					student:
+						monthly: "$6"
+						annual: "$60"
+					collaborator:
+						monthly: "$12"
+						annual: "$144"
+				EUR:
+					student:
+						monthly: "€5"
+						annual: "€50"
+					collaborator:
+						monthly: "€11"
+						annual: "€132"
+				GBP:
+					student:
+						monthly: "£5"
+						annual: "£50"
+					collaborator:
+						monthly: "£10"
+						annual: "£120"
+				SEK:
+					student:
+						monthly: "45 kr"
+						annual: "450 kr"
+					collaborator:
+						monthly: "90 kr"
+						annual: "1080 kr"
+				CAD:
+					student:
+						monthly: "$7"
+						annual: "$70"
+					collaborator:
+						monthly: "$14"
+						annual: "$168"
+				NOK:
+					student:
+						monthly: "45 kr"
+						annual: "450 kr"
+					collaborator:
+						monthly: "90 kr"
+						annual: "1080 kr"
+				DKK:
+					student:
+						monthly: "40 kr"
+						annual: "400 kr"
+					collaborator:
+						monthly: "70 kr"
+						annual: "840 kr"
+				AUD:
+					student:
+						monthly: "$8"
+						annual: "$80"
+					collaborator:
+						monthly: "$15"
+						annual: "$180"
+				NZD:
+					student:
+						monthly: "$8"
+						annual: "$80"
+					collaborator:
+						monthly: "$15"
+						annual: "$180"
+				CHF:
+					student:
+						monthly: "Fr 6"
+						annual: "Fr 60"
+					collaborator:
+						monthly: "Fr 12"
+						annual: "Fr 144"
+				SGD:
+					student:
+						monthly: "$8"
+						annual: "$80"
+					collaborator:
+						monthly: "$16"
+						annual: "$192"
+
+			ibis:
+				USD:
+					student:
+						monthly: "$10"
+						annual: "$100"
+					collaborator:
+						monthly: "$18"
+						annual: "$216"
+				EUR:
+					student:
+						monthly: "€9"
+						annual: "€90"
+					collaborator:
+						monthly: "€17"
+						annual: "€204"
+				GBP:
+					student:
+						monthly: "£7"
+						annual: "£70"
+					collaborator:
+						monthly: "£14"
+						annual: "£168"
+				SEK:
+					student:
+						monthly: "75 kr"
+						annual: "750 kr"
+					collaborator:
+						monthly: "140 kr"
+						annual: "1680 kr"
+				CAD:
+					student:
+						monthly: "$12"
+						annual: "$120"
+					collaborator:
+						monthly: "$22"
+						annual: "$264"
+				NOK:
+					student:
+						monthly: "75 kr"
+						annual: "750 kr"
+					collaborator:
+						monthly: "140 kr"
+						annual: "1680 kr"
+				DKK:
+					student:
+						monthly: "68 kr"
+						annual: "680 kr"
+					collaborator:
+						monthly: "110 kr"
+						annual: "1320 kr"
+				AUD:
+					student:
+						monthly: "$13"
+						annual: "$130"
+					collaborator:
+						monthly: "$22"
+						annual: "$264"
+				NZD:
+					student:
+						monthly: "$14"
+						annual: "$140"
+					collaborator:
+						monthly: "$22"
+						annual: "$264"
+				CHF:
+					student:
+						monthly: "Fr 10"
+						annual: "Fr 100"
+					collaborator:
+						monthly: "Fr 18"
+						annual: "Fr 216"
+				SGD:
+					student:
+						monthly: "$14"
+						annual: "$140"
+					collaborator:
+						monthly: "$25"
+						annual: "$300"
+
+			plans:
 				USD:
 					symbol: "$"
 					student:
@@ -23,7 +182,7 @@ define [
 						monthly: "$30"
 						annual: "$360"
 
-				EUR: 
+				EUR:
 					symbol: "€"
 					student:
 						monthly: "€7"
@@ -34,7 +193,7 @@ define [
 					professional:
 						monthly: "€28"
 						annual: "€336"
-						
+
 				GBP:
 					symbol: "£"
 					student:
@@ -117,7 +276,7 @@ define [
 					professional:
 						monthly: "$35"
 						annual: "$420"
-						
+
 				CHF:
 					symbol: "Fr"
 					student:
@@ -141,36 +300,54 @@ define [
 					professional:
 						monthly: "$40"
 						annual: "$480"
+
 		}
-	
 
 
+	App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack) ->
+
+		$scope.showPlans = false
 
-	App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http) ->
+		$scope.plansVariant = 'default'
+		$scope.shouldABTestPlans = window.shouldABTestPlans
+
+		if $scope.shouldABTestPlans
+			sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
+				$scope.plansVariant = chosenVariation
+				event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation}
+				if chosenVariation in ['heron', 'ibis']
+					# overwrite student plans with alternative
+					for currency, _v of $scope.plans
+						$scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student']
+						$scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator']
+				$scope.showPlans = true
+		else
+			$scope.showPlans = true
 
 		$scope.plans = MultiCurrencyPricing.plans
+
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 		$scope.trial_len = 7
+
 		$scope.planQueryString = '_free_trial_7_days'
 
 		$scope.ui =
 			view: "monthly"
 
-
 		$scope.changeCurreny = (newCurrency)->
 			$scope.currencyCode = newCurrency
 
 		$scope.signUpNowClicked = (plan, annual)->
+			event_tracking.sendMB 'plans-page-start-trial', {plan}
 			if $scope.ui.view == "annual"
 				plan = "#{plan}_annual"
-			
-			event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
+			event_tracking.send   'subscription-funnel', 'sign_up_now_button', plan
 
 		$scope.switchToMonthly = ->
 			$scope.ui.view = "monthly"
 			event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
-		
+
 		$scope.switchToStudent = ->
 			$scope.ui.view = "student"
 			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
@@ -178,7 +355,7 @@ define [
 		$scope.switchToAnnual = ->
 			$scope.ui.view = "annual"
 			event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
-			
+
 		$scope.openGroupPlanModal = () ->
 			$modal.open {
 				templateUrl: "groupPlanModalTemplate"

+ 26 - 11
services/web/public/coffee/main/subscription-dashboard.coffee

@@ -1,15 +1,21 @@
 define [
 	"base"
 ], (App)->
+
+	App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) ->
+		sixpack.convert 'plans-1610', () ->
+
+
 	SUBSCRIPTION_URL = "/user/subscription/update"
 
 	setupReturly = _.once ->
 		recurly?.configure window.recurlyApiKey
 	PRICES = {}
 
+
 	App.controller "CurrenyDropdownController", ($scope, MultiCurrencyPricing, $q)->
 
-		$scope.plans = MultiCurrencyPricing.plans
+		# $scope.plans = MultiCurrencyPricing.plans
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 		$scope.changeCurrency = (newCurrency)->
@@ -31,7 +37,7 @@ define [
 			$scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 		$scope.pricing = MultiCurrencyPricing
-		$scope.plans = MultiCurrencyPricing.plans
+		# $scope.plans = MultiCurrencyPricing.plans
 		$scope.currencySymbol = MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
 
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
@@ -53,9 +59,9 @@ define [
 			price = ""
 
 	App.controller "ConfirmChangePlanController", ($scope, $modalInstance, $http)->
-		
+
 		$scope.confirmChangePlan = ->
-			body = 
+			body =
 				plan_code: $scope.plan.planCode
 				_csrf : window.csrfToken
 
@@ -74,7 +80,7 @@ define [
 		$scope.confirmLeaveGroup = ->
 			$scope.inflight = true
 			$http({
-				url: "/subscription/group/user", 
+				url: "/subscription/group/user",
 				method: "DELETE",
 				params: {admin_user_id: $scope.admin_id, _csrf: window.csrfToken}
 			}).success ->
@@ -87,6 +93,8 @@ define [
 
 
 	App.controller "UserSubscriptionController", ($scope, MultiCurrencyPricing, $http, sixpack, $modal) ->
+		$scope.plans = MultiCurrencyPricing.plans
+
 		freeTrialEndDate = new Date(subscription?.trial_ends_at)
 
 		sevenDaysTime = new Date()
@@ -96,6 +104,16 @@ define [
 		freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
 
 		$scope.view = 'overview'
+		$scope.getSuffix = (planCode) ->
+			planCode?.match(/(.*?)_(.*)/)?[2] || null
+		$scope.subscriptionSuffix = $scope.getSuffix(window?.subscription?.planCode)
+		if $scope.subscriptionSuffix == 'free_trial_7_days'
+			$scope.subscriptionSuffix = ''
+		$scope.isNextGenPlan = $scope.subscriptionSuffix in ['heron', 'ibis']
+
+		$scope.shouldShowPlan = (planCode) ->
+			$scope.getSuffix(planCode) not in ['heron', 'ibis']
+
 		isMonthlyCollab = subscription?.planCode?.indexOf("collaborator") != -1 and subscription?.planCode?.indexOf("ann") == -1
 		stillInFreeTrial = freeTrialInFuture and freeTrialExpiresUnderSevenDays
 
@@ -118,7 +136,7 @@ define [
 					$scope.studentPrice = $scope.currencySymbol + (totalPriceExTax + taxAmmount)
 
 		$scope.downgradeToStudent = ->
-			body = 
+			body =
 				plan_code: 'student'
 				_csrf : window.csrfToken
 			$scope.inflight = true
@@ -129,7 +147,7 @@ define [
 					console.log "something went wrong changing plan"
 
 		$scope.cancelSubscription = ->
-			body = 
+			body =
 				_csrf : window.csrfToken
 
 			$scope.inflight = true
@@ -158,7 +176,7 @@ define [
 
 
 		$scope.exendTrial = ->
-			body = 
+			body =
 				_csrf : window.csrfToken
 			$scope.inflight = true
 			$http.put("/user/subscription/extend", body)
@@ -166,6 +184,3 @@ define [
 					location.reload()
 				.error ->
 					console.log "something went wrong changing plan"
-
-
-

BIN
services/web/public/img/teasers/history/teaser-history.png


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

@@ -205,128 +205,157 @@ 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_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();
+        suppressions = [];
+
+        for (var i = 0, len = hints.length; i<len; i++) {
+            var hint = hints[i];
+
+            var suppressedChanges = 0;
+            var hintRange = new Range(hint.start_row, hint.start_col, hint.end_row, hint.end_col);
+
+            var cursorInRange = hintRange.insideStart(cursor.row, cursor.column);
+            var cursorAtStart = hintRange.isStart(cursor.row, cursor.column);
+            var cursorAtEnd = hintRange.isEnd(cursor.row, cursor.column);
+            if (hint.suppressIfEditing && (cursorAtStart || cursorAtEnd)) {
+                suppressions.push(hintRange);
+                if (!hint.suppressed) { suppressedChanges++; };
+                hint.suppressed = true;
+                continue;
+            }
+            var isCascadeError = false;
+            for (var j = 0, suplen = suppressions.length; j < suplen; j++) {
+                var badRange = suppressions[j];
+                if (badRange.intersects(hintRange)) {
+                    isCascadeError = true;
+                    break;
+                }
+            }
+            if(isCascadeError) {
+                if (!hint.suppressed) { suppressedChanges++; };
+                hint.suppressed = true;
+                continue;
+            };
+
+            if (hint.suppressed) { suppressedChanges++; };
+            hint.suppressed = false;
+
+            annotations.push(hint);
+            if (hint.type === "info") {
+                continue;
+            };
+            var key = hintRange.toString() + (cursorInRange ? "+cursor" : "");
+            newRange[key] = {hint: hint, cursorInRange: cursorInRange, range: hintRange};
+        }
+        for (key in newRange) {
+            if (!savedRange[key]) {  // doesn't exist in already displayed errors
+                var new_range = newRange[key].range;
+                cursorInRange = newRange[key].cursorInRange;
+                hint = newRange[key].hint;
+                var errorAtStart = (hint.row === hint.start_row && hint.column === hint.start_col);
+                var a = (cursorInRange && !errorAtStart) ? cursorAnchor : doc.createAnchor(new_range.start);
+                var b = (cursorInRange && errorAtStart) ? cursorAnchor : doc.createAnchor(new_range.end);
+                var range = new Range();
+                range.start = a;
+                range.end = b;
+                var cssClass = "ace_error-marker";
+                if (hint.type === "warning") { cssClass = "ace_highlight-marker"; };
+                range.id = session.addMarker(range, cssClass, "text");
+                savedRange[key] = range;
+            }
+        }
+        for (key in savedRange) {
+            if (!newRange[key]) {  // no longer present in list of errors to display
+                range = savedRange[key];
+                if (range.start !== cursorAnchor) { range.start.detach(); }
+                if (range.end !== cursorAnchor) { range.end.detach(); }
+                session.removeMarker(range.id);
+                delete savedRange[key];
+            }
+        }
+        if (!cursorMoveOnly || suppressedChanges) {
+            if (firstPass) {
+                if (annotations.length > 0) {
+                    var originalAnnotations = session.getAnnotations();
+                    session.setAnnotations(originalAnnotations.concat(annotations));
+                };
+                firstPass = false;
+            } else {
+                session.setAnnotations(annotations);
+            }
+        };
+
+    };
+    worker.on("lint", function(results) {
+        if(docChangePending) { docChangePending = false; };
+        hints = results.data;
+        if (hints.length > 100) {
+            hints = hints.slice(0, 100); // limit to 100 errors
+        };
+        updateMarkers();
+    });
+    worker.on("terminate", function() {
+        if(changeHandler) {
+            clearTimeout(changeHandler);
+            changeHandler = null;
+        }
+        doc.off("change", docChangeHandler);
+        selection.off("changeCursor", cursorHandler);
+        for (var key in savedRange) {
+            var range = savedRange[key];
+            if (range.start !== cursorAnchor) { range.start.detach(); }
+            if (range.end !== cursorAnchor) { range.end.detach(); }
+            session.removeMarker(range.id);
+        }
+        savedRange = {};
+        hints = [];
+        suppressions = [];
+        session.clearAnnotations();
+    });
+
+    return worker;
+};
 
 var Mode = function() {
     this.HighlightRules = LatexHighlightRules;
     this.foldingRules = new LatexFoldMode();
-    this.createWorker = function(session) {
-	var doc = session.getDocument();
-	var selection = session.getSelection();
-
-	var savedRange = {};
-	var suppressions = [];
-	var hints = [];
-	var changeHandler = null;
-
-	var worker = new WorkerClient(["ace"], "ace/mode/latex_worker", "LatexWorker");
-	worker.attachToDocument(doc);
-
-	doc.on("change", function () {
-	    if(changeHandler) {
-		clearTimeout(changeHandler);
-		changeHandler = null;
-	    }
-	});
-
-	selection.on("changeCursor", function () {
-	    if(suppressions.length > 0) {
-		changeHandler = setTimeout(function () {
-		    updateMarkers();
-		    suppressions = [];
-		    changeHandler = null;
-		}, 100);
-	    }
-	});
-
-	var updateMarkers = function () {
-	    var annotations = [];
-	    var newRange = {};
-	    var cursor = selection.getCursor();
-	    suppressions = [];
-
-	    for (var i = 0; i<hints.length; i++) {
-		var data = hints[i];
-		var start_row = data.start_row;
-		var start_col = data.start_col;
-		var end_row = data.end_row;
-		var end_col = data.end_col;
-		if (data.suppressIfEditing &&
-		    ((cursor.row === start_row && cursor.column == start_col+1)
-		     || (cursor.row === end_row && (cursor.column+1) == end_col))) {
-		    suppressions.push([start_row, start_col, end_row, end_col]);
-		    continue;
-		}
-		var suppress = false;
-		for (var j = 0; j < suppressions.length; j++) {
-		    var e=suppressions[j];
-		    var fromRow=e[0], fromCol=e[1], toRow=e[2], toCol=e[3];
-		    if (start_row == fromRow && start_col >= fromCol && start_row === toRow  && start_col <= toCol) {
-			suppress = true;
-			break;
-		    }
-		}
-		if(suppress) { continue; };
-
-		var key = "(" + start_row + "," + start_col + ")" + ":" + "(" + end_row + "," + end_col + ")";
-		newRange[key] = data;
-		annotations.push(data);
-	    }
-
-	    var newKeys = Object.keys(newRange);
-	    var oldKeys = Object.keys(savedRange);
-	    var changes = 0;
-	    for (i = 0; i < newKeys.length; i++) {
-		key = newKeys[i];
-		if (!savedRange[key]) {
-		    var new_range = newRange[key];
-		    var a = doc.createAnchor(new_range.start_row, new_range.start_col);
-		    var b = doc.createAnchor(new_range.end_row, new_range.end_col);
-		    var range = new Range();
-		    range.start = a;
-		    range.end = b;
-		    range.id = session.addMarker(range, "ace_error-marker", "text");
-		    savedRange[key] = range;
-		    changes++;
-		}
-	    }
-
-	    for (i = 0; i < oldKeys.length; i++) {
-		key = oldKeys[i];
-		if (!newRange[key]) {
-		    range = savedRange[key];
-		    range.start.detach();
-		    range.end.detach();
-		    session.removeMarker(range.id);
-		    delete savedRange[key];
-		    changes++;
-		}
-	    }
-
-	    if (changes>0) {
-		session.setAnnotations(annotations);
-	    };
-	};
-
-	worker.on("lint", function(results) {
-	    hints = results.data;
-	    if (hints.length > 100) {
-		hints = hints.slice(0, 100); // limit to 100 errors
-	    };
-	    updateMarkers();
-	});
-
-	worker.on("terminate", function() {
-	    var oldKeys = Object.keys(savedRange);
-	    for (var i = 0; i < oldKeys.length; i++) {
-		var key = oldKeys[i];
-		var range = savedRange[key];
-		session.removeMarker(range.id);
-		delete savedRange[key];
-	    }
-
-	});
-	return worker;
-    };
+    this.createWorker = createLatexWorker;
 };
 oop.inherits(Mode, TextMode);
 

+ 607 - 380
services/web/public/js/ace-1.2.5/worker-latex.js

@@ -1419,407 +1419,634 @@ var LatexWorker = exports.LatexWorker = function(sender) {
 
 oop.inherits(LatexWorker, Mirror);
 
-var Parse = function (text) {
-    var errors = [];
-    var Comments = [];
+var Tokenise = function (text) {
     var Tokens = [];
-    var Environments = [];
+    var Comments = [];
     var pos = -1;
-    var SPECIAL = /[\\\{\}\$\&\#\^\_\~\%]/g;
-    var CS = /[^a-zA-Z]/g;
+    var SPECIAL = /[\\\{\}\$\&\#\^\_\~\%]/g;  // match TeX special characters
+    var NEXTCS = /[^a-zA-Z]/g;  // match characters which aren't part of a TeX control sequence
     var idx = 0;
-    var lineNumber = 0;
-    var linePosition = [];
-    linePosition[0] = 0;
 
-    var TokenError = function (token, message) {
-	var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
-	var start_col = start - linePosition[line];
-	var end_col = end - linePosition[line] + 1;
-	errors.push({row: line,
-		     column: start_col,
-		     start_row:line,
-		     start_col: start_col,
-		     end_row:line,
-		     end_col: end_col,
-		     type:"error",
-		     text:message,
-		     suppressIfEditing:true});
-    };
-
-    var TokenErrorFromTo = function (fromToken, toToken, message) {
-	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] + 1;
-
-	errors.push({row: line,
-		     column: start_col,
-		     start_row: fromLine,
-		     start_col: start_col,
-		     end_row: toLine,
-		     end_col: end_col,
-		     type:"error",
-		     text:message,
-		     suppressIfEditing:true});
-    };
-
-
-    var 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], fromSeq = fromToken[4];
-	if (!toToken) {toToken = fromToken;};
-	var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3], toSeq = toToken[4];
-	if (!toEnd) { toEnd = toStart + 1;};
-	var start_col = fromStart - linePosition[fromLine];
-	var end_col = toEnd - linePosition[toLine] + 1;
-	errors.push({row:toLine,
-		     column:end_col,
-		     start_row:fromLine,
-		     start_col: start_col,
-		     end_row:toLine,
-		     end_col: end_col,
-		     type:"error",
-		     text:message,
-		     suppressIfEditing:options.suppressIfEditing});
-    };
-
-    var EnvErrorTo = function (toEnv, message) {
-	var token = toEnv.closeToken || toEnv.token;
-	var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
-	if (!end) { end = start + 1; };
-	var end_col = end - linePosition[line] + 1;
-	var err = {row: line,
-		   column: end_col,
-		   start_row:0,
-		   start_col: 0,
-		   end_row: line,
-		   end_col: end_col,
-		   type:"error",
-		   text:message};
-	errors.push(err);
-    };
-
-    var EnvErrorFrom = function (env, message) {
-	var token = env.token;
-	var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
-	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:"error",
-		     text:message});
-    };
+    var lineNumber = 0;   // current line number when parsing tokens (zero-based)
+    var linePosition = [];  // mapping from line number to absolute offset of line in text[]
+    linePosition[0] = 0;
 
     var checkingDisabled = false;
+    var count = 0;  // number of tokens parses
+    var MAX_TOKENS = 100000;
 
     while (true) {
-	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) {
-	    break;
-	};
-	pos = result.index;
-	var newIdx = SPECIAL.lastIndex;
-	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;
-	    }
-	}
-	idx = newIdx;
-	var code = result[0];
-	if (code === "%") {
-	    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 === '\\') {
-	    CS.lastIndex = idx;
-	    var controlSequence = CS.exec(text);
-	    var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
-	    if (nextSpecialPos === idx) {
-		Tokens.push([lineNumber, code, pos, idx + 1, text[idx]]);
-		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 === "{") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else if (code === "}") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else if (code === "$") {
-	    if (text[idx] === "$") {
-		idx = SPECIAL.lastIndex = idx + 1;
-		Tokens.push([lineNumber, "$$", pos]);
-	    } else {
-		Tokens.push([lineNumber, code, pos]);
-	    }
-	} else if (code === "&") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else if (code === "#") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else if (code === "^") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else if (code === "_") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else if (code === "~") {
-	    Tokens.push([lineNumber, code, pos]);
-	} else {
-	    throw "unrecognised character " + code;
-	}
+        count++;
+        if (count > MAX_TOKENS) {
+            throw new Error("exceed max token count of " + MAX_TOKENS);
+            break;
+        };
+        var result = SPECIAL.exec(text);
+        if (result == null) {
+            if (idx < text.length) {
+                Tokens.push([lineNumber, "Text", idx, text.length]);
+            }
+            break;
+        }
+        if (result && result.index <= pos) {
+            throw new Error("infinite loop in parsing");
+            break;
+        };
+        pos = result.index;
+        if (pos > idx) {
+            Tokens.push([lineNumber, "Text", idx, pos]);
+        }
+        for (var i = idx; i < pos; i++) {
+            if (text[i] === "\n") {
+                lineNumber++;
+                linePosition[lineNumber] = i+1;
+            }
+        }
+
+        var newIdx = SPECIAL.lastIndex;
+        idx = newIdx;
+        var code = result[0];
+        if (code === "%") { // comment character
+            var newLinePos = text.indexOf("\n", idx);
+            if (newLinePos === -1) {
+                newLinePos = text.length;
+            };
+            var commentString = text.substring(idx, newLinePos);
+            if (commentString.indexOf("%novalidate") === 0) {
+                return [];
+            } else if(!checkingDisabled && commentString.indexOf("%begin novalidate") === 0) {
+                checkingDisabled = true;
+            } else if (checkingDisabled && commentString.indexOf("%end novalidate") === 0) {
+                checkingDisabled = false;
+            };
+            idx = SPECIAL.lastIndex = newLinePos + 1;
+            Comments.push([lineNumber, idx, newLinePos]);
+            lineNumber++;
+            linePosition[lineNumber] = idx;
+        } else if (checkingDisabled) {
+            continue;
+        } else if (code === '\\') { // escape character
+            NEXTCS.lastIndex = idx;
+            var controlSequence = NEXTCS.exec(text);
+            var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
+            if (nextSpecialPos === idx) {
+                Tokens.push([lineNumber, code, pos, idx + 1, text[idx]]);
+                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
+            if (text[idx] === "$") {
+                idx = SPECIAL.lastIndex = idx + 1;
+                Tokens.push([lineNumber, "$$", pos]);
+            } else {
+                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 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 readOptionalParams = function(TokeniseResult, k) {
+    var Tokens = TokeniseResult.tokens;
+    var text = TokeniseResult.text;
 
-    var read1arg = function (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];
-	    return k + 1;
-	} else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") {
-	    envName = env[4];
-	    return k + 3;
-	} else {
-	    return null;
-	}
-    };
-
-    var read1name = function (k) {
-	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 {
-	    return null;
-	}
-    };
-
-
-
-    var readOptionalParams = function(k) {
-	var params = Tokens[k+1];
-
-	if(params && params[1] === "Text") {
-	    var paramNum = text.substring(params[2], params[3]);
-	    if (paramNum.match(/^\[\d+\]$/)) {
-		return k + 1;
-	    };
-	};
-	return null;
-    };
-
-    var readDefinition = function(k) {
-	k = k + 1;
-	var count = 0;
-	var nextToken = Tokens[k];
-	while (nextToken && nextToken[1] === "Text") {
-	    var start = nextToken[2], end = nextToken[3];
-	    for (i = start; i < end; i++) {
-		var char = text[i];
-		if (char === ' ' || char === '\t' || char  === '\r' || char === '\n') { continue; }
-		return null;
-	    }
-	    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;
-    };
-
-    for (var _j = 0, _len = Tokens.length; _j < _len; _j++) {
-	var token = Tokens[_j];
-	var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
-	if (type === "\\") {
-	    if (seq === "begin" || seq === "end") {
-		var open = Tokens[_j+1];
-		var env = Tokens[_j+2];
-		var close = Tokens[_j+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});
-		    _j = _j + 3; // advance past these tokens
-		} else {
-		    var endToken = null;
-		    if (open && open[1] === "{") {
-			endToken = open;
-
-			if (env && env[1] === "Text") {
-			    endToken = env.slice();
-			    start = endToken[2]; end = endToken[3];
-			    for (i = start; i < end; i++) {
-				char = text[i];
-				if (char === ' ' || char === '\t' || char  === '\r' || char === '\n') { break; }
-			    }
-			    endToken[3] = i;
-			};
-		    };
-
-		    if (endToken) {
-			TokenErrorFromTo(token, endToken, "invalid environment command" + text.substring(token[2], endToken[3] || endToken[2]));
-		    } else {
-			TokenError(token, "invalid environment command");
-		    };
-		}
-	    } else if (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") {
-
-		var newPos = read1arg(_j);
-		if (newPos === null) { continue; } else {_j = newPos;};
-
-		newPos = readOptionalParams(_j);
-		if (newPos === null) { /* do nothing */ } else {_j = newPos;};
-
-		newPos = readDefinition(_j);
-		if (newPos === null) { /* do nothing */ } else {_j = newPos;};
-
-	    } else if (seq === "newenvironment") {
-
-		newPos = read1name(_j);
-		if (newPos === null) { continue; } else {_j = newPos;};
-
-		newPos = readOptionalParams(_j);
-		if (newPos === null) { /* do nothing */ } else {_j = newPos;};
-
-		newPos = readDefinition(_j);
-		if (newPos === null) { /* do nothing */ } else {_j = newPos;};
-
-		newPos = readDefinition(_j);
-		if (newPos === null) { /* do nothing */ } else {_j = newPos;};
-	    }
-	} else if (type === "{") {
-	    Environments.push({command:"{", token:token});
-	} else if (type === "}") {
-	    Environments.push({command:"}", token:token});
-	};
-
-	if ((start != null) && (end != null) && (seq != null)) {
-	} else if ((start != null) && (end != null)) {
-	} else if (start != null) {
-	} else {
-	}
+    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
+        };
+    };
+    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 = [];
+
+    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 === "\\") {
+            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 (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") {
+                var newPos = read1arg(TokeniseResult, i, {allowStar: (seq != "def")});
+                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 === "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 (type === "{") {
+            Environments.push({command:"{", token:token});
+        } else if (type === "}") {
+            Environments.push({command:"}", token:token});
+        };
+    };
+    return Environments;
+};
+
+
+var CheckEnvironments = function (Environments, ErrorReporter) {
+    var ErrorTo = ErrorReporter.EnvErrorTo;
+    var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
+    var ErrorFrom = ErrorReporter.EnvErrorFrom;
+
     var state = [];
-    for (i = 0; i < Environments.length; i++) {
-	var thisEnv = Environments[i];
-	if(thisEnv.command === "begin" || thisEnv.command === "{") {
-	    state.push(thisEnv);
-	} else if (thisEnv.command === "end" || thisEnv.command === "}") {
-	    var lastEnv = state.pop();
-	    if (!lastEnv) {
-		if (thisEnv.command === "}") {
-		    EnvErrorTo(thisEnv, "unexpected end group }");
-		} else if (thisEnv.command === "end") {
-		    EnvErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
-		}
-	    } else if (lastEnv.command === "{" && thisEnv.command === "}") {
-		continue; // closed group correctly
-	    } else if (lastEnv.name === thisEnv.name) {
-		continue; // closed environment correctly
-	    } else if (thisEnv.command === "}") {
-		EnvErrorFromTo(lastEnv, thisEnv, "unexpected end group } after \\begin{" + lastEnv.name +"}");
-		state.push(lastEnv);
-	    } else if (lastEnv.command === "{" && thisEnv.command === "end") {
-		EnvErrorFromTo(lastEnv, thisEnv, "unexpected \\end{" + thisEnv.name + "} inside group {", {suppressIfEditing:true});
-		i--;
-	    } else if (lastEnv.command === "begin" && thisEnv.command === "end") {
-		EnvErrorFromTo(lastEnv, thisEnv, "unexpected \\end{" + thisEnv.name + "} after \\begin{" + lastEnv.name + "}");
-		for (var j = i + 1; j < Environments.length; j++) {
-		    var futureEnv = Environments[j];
-		    if (futureEnv.command === "end" && futureEnv.name === lastEnv.name) {
-			state.push(lastEnv);
-			continue;
-		    }
-		}
-		lastEnv = state.pop();
-		if(lastEnv) {
-		    if (thisEnv.name === lastEnv.name) {
-			continue;
-		    } else {
-			state.push(lastEnv);
-		    }
-		}
-
-	    }
-	}
+    var documentClosed = null;
+    var inVerbatim = false;
+    var verbatimRanges = [];
+    for (var i = 0, len = Environments.length; i < len; i++) {
+        var name = Environments[i].name ;
+        if (name && name.match(/^(verbatim|boxedverbatim|lstlisting)$/)) {
+            Environments[i].verbatim = true;
+        }
+    }
+    for (i = 0, len = Environments.length; i < len; i++) {
+        var thisEnv = Environments[i];
+        if(thisEnv.command === "begin" || thisEnv.command === "{") {
+            if (inVerbatim) { continue; } // ignore anything in verbatim environments
+            if (thisEnv.verbatim) {inVerbatim = true;};
+            state.push(thisEnv);
+        } else if (thisEnv.command === "end" || thisEnv.command === "}") {
+            var lastEnv = state.pop();
+
+            if (inVerbatim) {
+                if (lastEnv && lastEnv.name === thisEnv.name) {
+                    inVerbatim = false;
+                    verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
+                    continue;
+                } else {
+                    if(lastEnv) { state.push(lastEnv); } ;
+                    continue;  // ignore all other commands
+                }
+            };
+
+            if (lastEnv && lastEnv.command === "{" && thisEnv.command === "}") {
+                continue;
+            } else if (lastEnv && lastEnv.name === thisEnv.name) {
+                if (thisEnv.name === "document" && !documentClosed) {
+                    documentClosed = thisEnv;
+                };
+                continue;
+            } else if (!lastEnv) {
+                if (thisEnv.command === "}") {
+                    if (documentClosed) {
+                        ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
+                    } else {
+                        ErrorTo(thisEnv, "unexpected end group }");
+                    };
+                } else if (thisEnv.command === "end") {
+                    if (documentClosed) {
+                        ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+                    } else {
+                        ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
+                    }
+                }
+            } else if (lastEnv.command === "begin" && thisEnv.command === "}") {
+                ErrorFromTo(lastEnv, thisEnv, "unexpected end group } after \\begin{" + lastEnv.name +"}");
+                state.push(lastEnv);
+            } else if (lastEnv.command === "{" && thisEnv.command === "end") {
+                ErrorFromTo(lastEnv, thisEnv,
+                            "unclosed group { found at \\end{" + thisEnv.name + "}",
+                            {suppressIfEditing:true, errorAtStart: true, type:"warning"});
+                i--;
+            } else if (lastEnv.command === "begin" && thisEnv.command === "end") {
+                ErrorFromTo(lastEnv, thisEnv,
+                            "unclosed \\begin{" + lastEnv.name + "} found at \\end{" + thisEnv.name + "} " ,
+                            {errorAtStart: true});
+                for (var j = i + 1; j < len; j++) {
+                    var futureEnv = Environments[j];
+                    if (futureEnv.command === "end" && futureEnv.name === lastEnv.name) {
+                        state.push(lastEnv);
+                        continue;
+                    }
+                }
+                lastEnv = state.pop();
+                if(lastEnv) {
+                    if (thisEnv.name === lastEnv.name) {
+                        continue;
+                    } else {
+                        state.push(lastEnv);
+                    }
+                }
+
+            }
+        }
     }
     while (state.length > 0) {
-	thisEnv = state.pop();
-	if (thisEnv.command === "{") {
-	    EnvErrorFrom(thisEnv, "unclosed group {");
-	} else if (thisEnv.command === "begin") {
-	    EnvErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
-	};
+        thisEnv = state.pop();
+        if (thisEnv.command === "{") {
+            ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
+        } else if (thisEnv.command === "begin") {
+            ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
+        };
+    }
+    var vlen = verbatimRanges.length;
+    len = ErrorReporter.tokenErrors.length;
+    if (vlen >0 && len > 0) {
+        for (i = 0; i < len; i++) {
+            var tokenError = ErrorReporter.tokenErrors[i];
+            var startPos = tokenError.startPos;
+            var endPos = tokenError.endPos;
+            for (j = 0; j < vlen; j++) {
+                if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
+                    tokenError.ignore = true;
+                    break;
+                }
+            }
+        }
     }
-    return errors;
+
+};
+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.getErrors = function () {
+        var returnedErrors = [];
+        for (var i = 0, len = tokenErrors.length; i < len; i++) {
+            if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
+        }
+        return returnedErrors.concat(errors);
+    };
+
+    this.TokenError = function (token, message) {
+        var line = token[0], type = token[1], start = token[2], end = token[3];
+        var start_col = start - linePosition[line];
+        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:true});
+    };
+
+    this.TokenErrorFromTo = function (fromToken, toToken, message) {
+        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:true});
+    };
+
+
+    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});
+    };
+
+    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};
+        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});
+    };
+};
+
+var Parse = function (text) {
+    var TokeniseResult = Tokenise(text);
+    var Reporter = new ErrorReporter(TokeniseResult);
+    var Environments = InterpretTokens(TokeniseResult, Reporter);
+    CheckEnvironments(Environments, Reporter);
+    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);
+        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);

+ 1 - 0
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/.gitattributes

@@ -0,0 +1 @@
+* binary

BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-EUC-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-EUC-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-RKSJ-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78ms-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/78ms-RKSJ-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/83pv-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90ms-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90ms-RKSJ-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90msp-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90msp-RKSJ-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90pv-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/90pv-RKSJ-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-RKSJ-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-RKSJ-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Add-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-0.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-1.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-3.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-4.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-5.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-6.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-CNS1-UCS2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-0.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-1.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-3.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-4.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-5.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-GB1-UCS2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-0.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-1.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-3.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-4.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-5.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-6.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Japan1-UCS2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-0.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-1.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/Adobe-Korea1-UCS2.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5pc-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/B5pc-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS-EUC-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS-EUC-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS1-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS1-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS2-H.bcmap


+ 3 - 0
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/CNS2-V.bcmap

@@ -0,0 +1,3 @@
+àRCopyright 1990-2009 Adobe Systems Incorporated.
+All rights reserved.
+See ./LICENSEáCNS2-H

BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETHK-B5-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETHK-B5-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETen-B5-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETen-B5-V.bcmap


+ 3 - 0
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETenms-B5-H.bcmap

@@ -0,0 +1,3 @@
+àRCopyright 1990-2009 Adobe Systems Incorporated.
+All rights reserved.
+See ./LICENSEá	ETen-B5-H` ^

BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/ETenms-B5-V.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/EUC-H.bcmap


BIN
services/web/public/js/libs/pdfjs-1.6.210p1/bcmaps/EUC-V.bcmap


Некоторые файлы не были показаны из-за большого количества измененных файлов