From 73e0a636892ecc56d9bb7b62c7579ef2cf67a04a Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 29 Nov 2016 17:12:53 +0000 Subject: [PATCH 01/31] Basic mark-up and styles. Change the controller to hold all notifications. --- services/web/app/views/project/list.jade | 18 +++++-- .../public/coffee/main/announcements.coffee | 24 ++++----- .../public/stylesheets/app/project-list.less | 54 +++++++++++++++++++ 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 6f1d87424a..dc4bff9444 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -22,9 +22,21 @@ block content .content.content-alt(ng-controller="ProjectPageController") .container - //- div(ng-controller="AnnouncementsController", ng-cloak) - //- .alert.alert-success(ng-show="dataRecived") - //- a(href, ng-click="openLink()") {{title}} and {{totalAnnouncements}} others + div( + ng-controller="AnnouncementsController" + ng-cloak + ) + .announcements(ng-if="announcements.length") + span.announcements-badge {{ announcements.length }} + .announcements-body + .announcement( + ng-repeat="announcement in announcements" + ) + a.announcement-title(href, ng-click="openLink()") {{ announcement.title }} + p.announcement-description {{ announcement.excerpt }} + //- .card.card-thin(ng-show="dataReceived") + //- a.announcement-title(href, ng-click="openLink()") {{ title }} + //- p.announcement-description {{ excerpt }} .row(ng-cloak) span(ng-if="projects.length > 0") diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee index b0960bcc09..36a1185d0c 100644 --- a/services/web/public/coffee/main/announcements.coffee +++ b/services/web/public/coffee/main/announcements.coffee @@ -2,19 +2,19 @@ define [ "base" ], (App) -> App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window) -> + $scope.announcements = [] - $scope.dataRecived = false - announcement = null - $http.get("/announcements").success (announcements) -> - if announcements?[0]? - announcement = announcements[0] - $scope.title = announcement.title - $scope.totalAnnouncements = announcements.length - $scope.dataRecived = true + refreshAnnouncements = -> + $http.get("/announcements").success (announcements) -> + $scope.announcements = announcements + + dismissCurrentAnnouncement = -> + event_tracking.sendMB "announcement-alert-dismissed", { blogPostId:announcement.id } - dismissannouncement = -> - event_tracking.sendMB "announcement-alert-dismissed", {blogPostId:announcement.id} + refreshAnnouncements() $scope.openLink = -> - dismissannouncement() - $window.location.href = announcement.url + dismissCurrentAnnouncement() + .then(refreshAnnouncements) + + $window.open = announcement.url diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index de3307a017..1556252d2d 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -1,3 +1,14 @@ +@announcements-shadow: 0 2px 4px 1px rgba(0, 0, 0, 0.2); + +@keyframes pulse { + 0% { + opacity: .7; + } + 100% { + opacity: .9; + } +} + .project-header { .btn-group > .btn { padding-left: @line-height-base / 2; @@ -293,3 +304,46 @@ ul.project-list { margin-left:-100px; } } + +.announcements { + position: absolute; + bottom: 100px; + right: 100px; + width: 80px; + height: 80px; + background: url(/img/lion-128.png) no-repeat center/80% white; + border-radius: 50%; + box-shadow: @announcements-shadow; + z-index: 1; +} + + .announcements-badge { + display: inline-block; + position: absolute; + font-size: 11px; + height: 1.8em; + min-width: 1.8em; + border-radius: 0.9em; + line-height: 1.8; + padding: 0 2px; + top: 1px; + right: 1px; + font-weight: bold; + color: #FFF; + background-color: @red; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + animation: pulse 1s alternate infinite; + } + + .announcements-body { + position: absolute; + right: 0; + bottom: 80px; + width: 500px; + min-height: 100px; + background: #FFF; + box-shadow: @announcements-shadow; + border-radius: 10px; + } From a00acd7458e9cc9adf213404813a52f2254e6a7c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 5 Dec 2016 15:32:38 +0000 Subject: [PATCH 02/31] Add animations and state toggling. --- services/web/app/views/project/list.jade | 37 ++++-- .../public/coffee/main/announcements.coffee | 5 +- .../public/stylesheets/app/project-list.less | 124 +++++++++++++----- 3 files changed, 118 insertions(+), 48 deletions(-) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index dc4bff9444..411536316c 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -19,24 +19,37 @@ block content } }; - .content.content-alt(ng-controller="ProjectPageController") + .content.content-alt.project-list-page(ng-controller="ProjectPageController") .container div( ng-controller="AnnouncementsController" ng-cloak ) - .announcements(ng-if="announcements.length") - span.announcements-badge {{ announcements.length }} - .announcements-body - .announcement( - ng-repeat="announcement in announcements" - ) - a.announcement-title(href, ng-click="openLink()") {{ announcement.title }} - p.announcement-description {{ announcement.excerpt }} - //- .card.card-thin(ng-show="dataReceived") - //- a.announcement-title(href, ng-click="openLink()") {{ title }} - //- p.announcement-description {{ excerpt }} + .announcements-backdrop(ng-if="ui.isOpen") + a.announcements-btn( + href + ng-if="announcements.length" + ng-click="ui.isOpen = !ui.isOpen" + ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.hasNew }" + ) + span.announcements-badge(ng-if="ui.hasNew") {{ announcements.length }} + .announcements-body( + ng-if="ui.isOpen" + ) + .announcement( + ng-repeat="announcement in announcements" + ) + h2.announcement-header {{ announcement.title }} + p.announcement-date {{ announcement.date | date:"longDate" }} + p.announcement-description {{ announcement.excerpt }} + a( + ng-href="{{ announcement.url }}" + target="_blank" + ) Read more + //- .card.card-thin(ng-show="dataReceived") + //- a.announcement-title(href, ng-click="openLink()") {{ title }} + //- p.announcement-description {{ excerpt }} .row(ng-cloak) span(ng-if="projects.length > 0") diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee index 36a1185d0c..910658ad2e 100644 --- a/services/web/public/coffee/main/announcements.coffee +++ b/services/web/public/coffee/main/announcements.coffee @@ -3,7 +3,10 @@ define [ ], (App) -> App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window) -> $scope.announcements = [] - + $scope.ui = + isOpen: false + hasNew: false + refreshAnnouncements = -> $http.get("/announcements").success (announcements) -> $scope.announcements = announcements diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 1556252d2d..0bbe7e1e2c 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -8,6 +8,19 @@ opacity: .9; } } +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.project-list-page { + position: relative; + overflow: hidden; +} .project-header { .btn-group > .btn { @@ -305,45 +318,86 @@ ul.project-list { } } -.announcements { - position: absolute; - bottom: 100px; - right: 100px; - width: 80px; - height: 80px; - background: url(/img/lion-128.png) no-repeat center/80% white; - border-radius: 50%; - box-shadow: @announcements-shadow; +.announcements-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.35); + opacity: 0; + animation: fade-in 0.35s forwards; z-index: 1; } - .announcements-badge { - display: inline-block; - position: absolute; - font-size: 11px; - height: 1.8em; - min-width: 1.8em; - border-radius: 0.9em; - line-height: 1.8; - padding: 0 2px; - top: 1px; - right: 1px; - font-weight: bold; - color: #FFF; - background-color: @red; - vertical-align: baseline; - white-space: nowrap; - text-align: center; - animation: pulse 1s alternate infinite; +.announcements-btn { + position: absolute; + bottom: -50px; + right: 3%; + width: 80px; + height: 80px; + background: url(/img/lion-128.png) no-repeat center/80% transparent; + border-radius: 50%; + box-shadow: none; + z-index: 1; + transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55), + background 0.25s ease, + box-shadow 0.25s ease; + + &:hover { + bottom: -45px; } - .announcements-body { - position: absolute; - right: 0; - bottom: 80px; - width: 500px; - min-height: 100px; - background: #FFF; + &-open, &-open:hover, + &-has-new, &-has-new:hover { + background-color: #FFF; box-shadow: @announcements-shadow; - border-radius: 10px; + bottom: 40px; } +} +.announcements-badge { + display: inline-block; + position: absolute; + font-size: 11px; + height: 1.8em; + min-width: 1.8em; + border-radius: 0.9em; + line-height: 1.8; + padding: 0 2px; + top: 1px; + right: 1px; + font-weight: bold; + color: #FFF; + background-color: @red; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + z-index: 1; + animation: pulse 1s alternate infinite; +} + +.announcements-body { + position: absolute; + right: 3%; + bottom: 130px; + width: 700px; + min-height: 100px; + background: #FFF; + z-index: 1; + box-shadow: @announcements-shadow; + padding: 0 @line-height-computed; + border-radius: @border-radius-base; + animation: fade-in 0.35s forwards; +} + .announcement { + margin-bottom: @line-height-computed; + } + .announcement-header { + .page-header; + margin-bottom: 0; + } + + .announcement-date { + .small; + margin: (@line-height-computed / 4) 0; + } From 6a9ab1f525513830623be2b1027a7cd5e96c9b71 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 6 Dec 2016 14:17:30 +0000 Subject: [PATCH 03/31] Style adjusments; make announcements close when backdrop is clicked. --- services/web/app/views/project/list.jade | 23 ++-- .../public/stylesheets/app/project-list.less | 105 ++++++++++++------ 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 411536316c..e4ba2ae3f2 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -21,12 +21,15 @@ block content .content.content-alt.project-list-page(ng-controller="ProjectPageController") .container - - div( + .announcements( ng-controller="AnnouncementsController" + ng-class="{ 'announcements-open': ui.isOpen }" ng-cloak ) - .announcements-backdrop(ng-if="ui.isOpen") + .announcements-backdrop( + ng-if="ui.isOpen" + ng-click="ui.isOpen = false;" + ) a.announcements-btn( href ng-if="announcements.length" @@ -41,15 +44,13 @@ block content ng-repeat="announcement in announcements" ) h2.announcement-header {{ announcement.title }} - p.announcement-date {{ announcement.date | date:"longDate" }} p.announcement-description {{ announcement.excerpt }} - a( - ng-href="{{ announcement.url }}" - target="_blank" - ) Read more - //- .card.card-thin(ng-show="dataReceived") - //- a.announcement-title(href, ng-click="openLink()") {{ title }} - //- p.announcement-description {{ excerpt }} + .announcement-meta + p.announcement-date {{ announcement.date | date:"longDate" }} + a.announcement-link( + ng-href="{{ announcement.url }}" + target="_blank" + ) Read more .row(ng-cloak) span(ng-if="projects.length > 0") diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 0bbe7e1e2c..796ce3e747 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -1,4 +1,4 @@ -@announcements-shadow: 0 2px 4px 1px rgba(0, 0, 0, 0.2); +@announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5); @keyframes pulse { 0% { @@ -318,6 +318,12 @@ ul.project-list { } } +.announcements { + position: absolute; + bottom: 0; + right: 3%; +} + .announcements-backdrop { position: fixed; top: 0; @@ -333,7 +339,7 @@ ul.project-list { .announcements-btn { position: absolute; bottom: -50px; - right: 3%; + right: 0; width: 80px; height: 80px; background: url(/img/lion-128.png) no-repeat center/80% transparent; @@ -352,52 +358,83 @@ ul.project-list { &-has-new, &-has-new:hover { background-color: #FFF; box-shadow: @announcements-shadow; - bottom: 40px; + bottom: 30px; } } -.announcements-badge { - display: inline-block; - position: absolute; - font-size: 11px; - height: 1.8em; - min-width: 1.8em; - border-radius: 0.9em; - line-height: 1.8; - padding: 0 2px; - top: 1px; - right: 1px; - font-weight: bold; - color: #FFF; - background-color: @red; - vertical-align: baseline; - white-space: nowrap; - text-align: center; - z-index: 1; - animation: pulse 1s alternate infinite; -} + .announcements-badge { + display: inline-block; + position: absolute; + font-size: 11px; + height: 1.8em; + min-width: 1.8em; + border-radius: 0.9em; + line-height: 1.8; + padding: 0 2px; + top: 1px; + right: 1px; + font-weight: bold; + color: #FFF; + background-color: @red; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + z-index: 1; + animation: pulse 1s alternate infinite; + } .announcements-body { position: absolute; - right: 3%; - bottom: 130px; + right: 95px; + bottom: 30px; width: 700px; min-height: 100px; background: #FFF; z-index: 1; box-shadow: @announcements-shadow; - padding: 0 @line-height-computed; + padding: @line-height-computed; border-radius: @border-radius-base; animation: fade-in 0.35s forwards; + + &::after { + content: "\25b8"; + position: absolute; + left: 100%; + bottom: 17px; + width: 30px; + color: #FFF; + text-shadow: @announcements-shadow; + font-size: 2em; + overflow: hidden; + text-indent: -6px; + } } .announcement { - margin-bottom: @line-height-computed; - } - .announcement-header { - .page-header; + margin-bottom: @line-height-computed * 1.5; + &:last-child { margin-bottom: 0; } - - .announcement-date { - .small; - margin: (@line-height-computed / 4) 0; + } + .announcement-header { + .page-header; + margin: 0; } + + .announcement-description { + margin: (@line-height-computed / 4) 0 (@line-height-computed / 2); + } + + .announcement-meta { + .clearfix; + font-size: 0.9em; + } + + .announcement-date { + float: left; + color: @gray; + margin: 0; + } + + .announcement-link { + float: right; + margin: 0; + } From 5d9ae252829c9a07a6a89c6347b74bf987fd77f0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 6 Dec 2016 15:41:58 +0000 Subject: [PATCH 04/31] Allow announcements to overflow the header. --- .../web/public/stylesheets/app/project-list.less | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 796ce3e747..d2562d4546 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -19,7 +19,6 @@ .project-list-page { position: relative; - overflow: hidden; } .project-header { @@ -320,8 +319,12 @@ ul.project-list { .announcements { position: absolute; + top: -100%; bottom: 0; - right: 3%; + right: 0; + height: auto; + width: 100%; + overflow: hidden; } .announcements-backdrop { @@ -339,7 +342,7 @@ ul.project-list { .announcements-btn { position: absolute; bottom: -50px; - right: 0; + right: 3%; width: 80px; height: 80px; background: url(/img/lion-128.png) no-repeat center/80% transparent; @@ -384,7 +387,8 @@ ul.project-list { .announcements-body { position: absolute; - right: 95px; + right: 3%; + margin-right: 95px; bottom: 30px; width: 700px; min-height: 100px; From 821e556dd32e80b98a750c6f74b5af4b476cb792 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 6 Dec 2016 17:15:56 +0000 Subject: [PATCH 05/31] Better handling of overflowing content. --- services/web/app/views/project/list.jade | 23 ++++++++++--------- .../public/stylesheets/app/project-list.less | 12 ++++++++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index e4ba2ae3f2..1311b1a2c8 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -40,17 +40,18 @@ block content .announcements-body( ng-if="ui.isOpen" ) - .announcement( - ng-repeat="announcement in announcements" - ) - h2.announcement-header {{ announcement.title }} - p.announcement-description {{ announcement.excerpt }} - .announcement-meta - p.announcement-date {{ announcement.date | date:"longDate" }} - a.announcement-link( - ng-href="{{ announcement.url }}" - target="_blank" - ) Read more + .announcements-scroller + .announcement( + ng-repeat="announcement in announcements" + ) + h2.announcement-header {{ announcement.title }} + p.announcement-description {{ announcement.excerpt }} + .announcement-meta + p.announcement-date {{ announcement.date | date:"longDate" }} + a.announcement-link( + ng-href="{{ announcement.url }}" + target="_blank" + ) Read more .row(ng-cloak) span(ng-if="projects.length > 0") diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index d2562d4546..e9aa20d2da 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -319,12 +319,18 @@ ul.project-list { .announcements { position: absolute; - top: -100%; bottom: 0; right: 0; - height: auto; + height: 150px; width: 100%; + pointer-events: none; overflow: hidden; + + &-open { + top: -100%; + height: auto; + pointer-events: all; + } } .announcements-backdrop { @@ -349,6 +355,7 @@ ul.project-list { border-radius: 50%; box-shadow: none; z-index: 1; + pointer-events: all; transition: bottom 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55), background 0.25s ease, box-shadow 0.25s ease; @@ -391,6 +398,7 @@ ul.project-list { margin-right: 95px; bottom: 30px; width: 700px; + max-height: 52%; min-height: 100px; background: #FFF; z-index: 1; From 3595aff5e2451947ab4213d1a0192b62a8be16e4 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 7 Dec 2016 11:10:02 +0000 Subject: [PATCH 06/31] Scrolling behaviour. --- .../public/stylesheets/app/project-list.less | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index e9aa20d2da..50d02c251e 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -393,6 +393,8 @@ ul.project-list { } .announcements-body { + display: flex; + align-items: stretch; position: absolute; right: 3%; margin-right: 95px; @@ -403,7 +405,6 @@ ul.project-list { background: #FFF; z-index: 1; box-shadow: @announcements-shadow; - padding: @line-height-computed; border-radius: @border-radius-base; animation: fade-in 0.35s forwards; @@ -417,36 +418,43 @@ ul.project-list { text-shadow: @announcements-shadow; font-size: 2em; overflow: hidden; - text-indent: -6px; + text-indent: -7px; } } - .announcement { - margin-bottom: @line-height-computed * 1.5; - &:last-child { - margin-bottom: 0; - } - } - .announcement-header { - .page-header; - margin: 0; - } - .announcement-description { - margin: (@line-height-computed / 4) 0 (@line-height-computed / 2); - } - - .announcement-meta { - .clearfix; - font-size: 0.9em; - } - - .announcement-date { - float: left; - color: @gray; + .announcements-scroller { + padding: @line-height-computed; + flex-grow: 0; + overflow-x: hidden; + overflow-y: auto; + } + .announcement { + margin-bottom: @line-height-computed * 1.5; + &:last-child { + margin-bottom: 0; + } + } + .announcement-header { + .page-header; margin: 0; } - .announcement-link { - float: right; - margin: 0; + .announcement-description { + margin: (@line-height-computed / 4) 0 (@line-height-computed / 2); } + + .announcement-meta { + .clearfix; + font-size: 0.9em; + } + + .announcement-date { + float: left; + color: @gray; + margin: 0; + } + + .announcement-link { + float: right; + margin: 0; + } From e6eeb86046d6d72dd05d7475c8291fcf8daf9f92 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 7 Dec 2016 11:39:22 +0000 Subject: [PATCH 07/31] add read property to announcments --- .../Announcements/AnnouncementsHandler.coffee | 11 +++++-- .../AnnouncementsHandlerTests.coffee | 29 +++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee index a27608343a..ce41e3b96c 100644 --- a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee +++ b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee @@ -25,8 +25,15 @@ module.exports = announcementIndex = _.findIndex announcements, (announcement)-> announcement.id == lastSeenBlogId - if announcementIndex != -1 - announcements = announcements.slice(0, announcementIndex) + announcements = _.map announcements, (announcement, index)-> + if announcementIndex == -1 + read = false + else if index >= announcementIndex + read = true + else + read = false + announcement.read = read + return announcement logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements" diff --git a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee index 7578ea3a64..49e8292f97 100644 --- a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee @@ -42,10 +42,13 @@ describe 'AnnouncementsHandler', -> @BlogHandler.getLatestAnnouncements.callsArgWith(0, null, @stubbedAnnouncements) - it "should return all announcements if there are no getLastOccurance", (done)-> + it "should mark all announcements as read is false", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, []) @handler.getUnreadAnnouncements @user_id, (err, announcements)=> - announcements.length.should.equal 4 + announcements[0].read.should.equal false + announcements[1].read.should.equal false + announcements[2].read.should.equal false + announcements[3].read.should.equal false done() it "should should be sorted again to ensure correct order", (done)-> @@ -57,16 +60,30 @@ describe 'AnnouncementsHandler', -> announcements[0].should.equal @stubbedAnnouncements[0] done() - it "should return ones older than the last blog id", (done)-> + it "should return older ones marked as read as well", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2014/04/12/title-date-irrelivant"}}) @handler.getUnreadAnnouncements @user_id, (err, announcements)=> - announcements.length.should.equal 2 announcements[0].id.should.equal @stubbedAnnouncements[0].id + announcements[0].read.should.equal false + announcements[1].id.should.equal @stubbedAnnouncements[1].id + announcements[1].read.should.equal false + + announcements[2].id.should.equal @stubbedAnnouncements[3].id + announcements[2].read.should.equal true + + announcements[3].id.should.equal @stubbedAnnouncements[2].id + announcements[3].read.should.equal true + done() - it "should return none when the latest id is the first element", (done)-> + it "should return all of them marked as read", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}}) @handler.getUnreadAnnouncements @user_id, (err, announcements)=> - announcements.length.should.equal 0 + announcements[0].read.should.equal true + announcements[1].read.should.equal true + announcements[2].read.should.equal true + announcements[3].read.should.equal true done() + + From c716b86def75cc13fe6eeb340a6864e56ba6ddc7 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 7 Dec 2016 14:26:10 +0000 Subject: [PATCH 08/31] Add logic to dismiss unread announcements, styling fixes. --- services/web/app/views/project/list.jade | 10 ++++----- .../public/coffee/main/announcements.coffee | 22 +++++++++++-------- .../public/stylesheets/app/project-list.less | 1 + 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 1311b1a2c8..476ef0ca4c 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -28,21 +28,21 @@ block content ) .announcements-backdrop( ng-if="ui.isOpen" - ng-click="ui.isOpen = false;" + ng-click="toggleAnnouncementsUI();" ) a.announcements-btn( href ng-if="announcements.length" - ng-click="ui.isOpen = !ui.isOpen" - ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.hasNew }" + ng-click="toggleAnnouncementsUI();" + ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.newItems }" ) - span.announcements-badge(ng-if="ui.hasNew") {{ announcements.length }} + span.announcements-badge(ng-if="ui.newItems") {{ ui.newItems }} .announcements-body( ng-if="ui.isOpen" ) .announcements-scroller .announcement( - ng-repeat="announcement in announcements" + ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id" ) h2.announcement-header {{ announcement.title }} p.announcement-description {{ announcement.excerpt }} diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee index 910658ad2e..94303e921a 100644 --- a/services/web/public/coffee/main/announcements.coffee +++ b/services/web/public/coffee/main/announcements.coffee @@ -1,23 +1,27 @@ define [ "base" ], (App) -> - App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window) -> + App.controller "AnnouncementsController", ($scope, $http, event_tracking, $window, _) -> $scope.announcements = [] $scope.ui = isOpen: false - hasNew: false - + newItems: 0 + refreshAnnouncements = -> $http.get("/announcements").success (announcements) -> $scope.announcements = announcements + $scope.ui.newItems = _.filter(announcements, (announcement) -> !announcement.read).length - dismissCurrentAnnouncement = -> - event_tracking.sendMB "announcement-alert-dismissed", { blogPostId:announcement.id } + markAnnouncementsAsRead = -> + event_tracking.sendMB "announcement-alert-dismissed", { blogPostId: $scope.announcements[0].id } refreshAnnouncements() - $scope.openLink = -> - dismissCurrentAnnouncement() - .then(refreshAnnouncements) + $scope.toggleAnnouncementsUI = -> + $scope.ui.isOpen = !$scope.ui.isOpen + + if !$scope.ui.isOpen and $scope.ui.newItems + $scope.ui.newItems = 0 + markAnnouncementsAsRead() + - $window.open = announcement.url diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 50d02c251e..5e03c1facc 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -394,6 +394,7 @@ ul.project-list { .announcements-body { display: flex; + flex-direction: column; align-items: stretch; position: absolute; right: 3%; From 58ab888f3f915df8b2272fce5e076f92d9a3fd85 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 7 Dec 2016 14:57:54 +0000 Subject: [PATCH 09/31] Add a button to show all, when filtering new items. --- services/web/app/views/project/list.jade | 7 +++++++ services/web/public/coffee/main/announcements.coffee | 2 ++ 2 files changed, 9 insertions(+) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 476ef0ca4c..ac852da97e 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -52,6 +52,13 @@ block content ng-href="{{ announcement.url }}" target="_blank" ) Read more + div.text-center( + ng-if="ui.newItems > 0 && ui.newItems < announcements.length" + ) + a.btn.btn-default.btn-sm( + href + ng-click="showAll();" + ) Show all .row(ng-cloak) span(ng-if="projects.length > 0") diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee index 94303e921a..cccebf56ad 100644 --- a/services/web/public/coffee/main/announcements.coffee +++ b/services/web/public/coffee/main/announcements.coffee @@ -24,4 +24,6 @@ define [ $scope.ui.newItems = 0 markAnnouncementsAsRead() + $scope.showAll = -> + $scope.ui.newItems = 0 From 42bd1852c6c8a3b99dd16746ffad7ab4946a0066 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 7 Dec 2016 15:45:22 +0000 Subject: [PATCH 10/31] Use HTML binding in the excerpt. --- services/web/app/views/project/list.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index ac852da97e..d9fbf0e6b1 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -45,7 +45,7 @@ block content ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id" ) h2.announcement-header {{ announcement.title }} - p.announcement-description {{ announcement.excerpt }} + p.announcement-description(ng-bind-html="announcement.excerpt") .announcement-meta p.announcement-date {{ announcement.date | date:"longDate" }} a.announcement-link( From 6e8ee826dce05f5bc78cc411c7aa22f2ae1aad78 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 12 Dec 2016 10:10:49 +0000 Subject: [PATCH 11/31] update latex syntax checker to include mathmode from our ace commit 21dde1e5f104b02e19534e56d7eb2dbd3a950db9 provide it as a beta release first --- .../ide/editor/directives/aceEditor.coffee | 4 + .../public/js/ace-1.2.5/mode-latex_beta.js | 378 ++ .../js/ace-1.2.5/snippets/latex_beta.js | 7 + .../public/js/ace-1.2.5/worker-latex_beta.js | 3052 +++++++++++++++++ 4 files changed, 3441 insertions(+) create mode 100644 services/web/public/js/ace-1.2.5/mode-latex_beta.js create mode 100644 services/web/public/js/ace-1.2.5/snippets/latex_beta.js create mode 100644 services/web/public/js/ace-1.2.5/worker-latex_beta.js diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 885deec2f8..b17f3a1268 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -270,6 +270,10 @@ define [ catch mode = "ace/mode/plain_text" + # Give beta users the next release of the syntax checker + if mode is "ace/mode/latex" and window.user?.betaProgram + mode = "ace/mode/latex_beta" + # create our new session session = new EditSession(lines, mode) diff --git a/services/web/public/js/ace-1.2.5/mode-latex_beta.js b/services/web/public/js/ace-1.2.5/mode-latex_beta.js new file mode 100644 index 0000000000..1a98491951 --- /dev/null +++ b/services/web/public/js/ace-1.2.5/mode-latex_beta.js @@ -0,0 +1,378 @@ +ace.define("ace/mode/latex_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + +var LatexHighlightRules = function() { + + this.$rules = { + "start" : [{ + token : "comment", + regex : "%.*$" + }, { + token : ["keyword", "lparen", "variable.parameter", "rparen", "lparen", "storage.type", "rparen"], + regex : "(\\\\(?:documentclass|usepackage|input))(?:(\\[)([^\\]]*)(\\]))?({)([^}]*)(})" + }, { + token : ["keyword","lparen", "variable.parameter", "rparen"], + regex : "(\\\\(?:label|v?ref|cite(?:[^{]*)))(?:({)([^}]*)(}))?" + }, { + token : ["storage.type", "lparen", "variable.parameter", "rparen"], + regex : "(\\\\(?:begin|end))({)(\\w*)(})" + }, { + token : "storage.type", + regex : "\\\\[a-zA-Z]+" + }, { + token : "lparen", + regex : "[[({]" + }, { + token : "rparen", + regex : "[\\])}]" + }, { + token : "constant.character.escape", + regex : "\\\\[^a-zA-Z]?" + }, { + token : "string", + regex : "\\${1,2}", + next : "equation" + }], + "equation" : [{ + token : "comment", + regex : "%.*$" + }, { + token : "string", + regex : "\\${1,2}", + next : "start" + }, { + token : "constant.character.escape", + regex : "\\\\(?:[^a-zA-Z]|[a-zA-Z]+)" + }, { + token : "error", + regex : "^\\s*$", + next : "start" + }, { + defaultToken : "string" + }] + + }; +}; +oop.inherits(LatexHighlightRules, TextHighlightRules); + +exports.LatexHighlightRules = LatexHighlightRules; + +}); + +ace.define("ace/mode/folding/latex",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode","ace/range","ace/token_iterator"], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var BaseFoldMode = require("./fold_mode").FoldMode; +var Range = require("../../range").Range; +var TokenIterator = require("../../token_iterator").TokenIterator; + +var FoldMode = exports.FoldMode = function() {}; + +oop.inherits(FoldMode, BaseFoldMode); + +(function() { + + this.foldingStartMarker = /^\s*\\(begin)|(section|subsection|paragraph)\b|{\s*$/; + this.foldingStopMarker = /^\s*\\(end)\b|^\s*}/; + + this.getFoldWidgetRange = function(session, foldStyle, row) { + var line = session.doc.getLine(row); + var match = this.foldingStartMarker.exec(line); + if (match) { + if (match[1]) + return this.latexBlock(session, row, match[0].length - 1); + if (match[2]) + return this.latexSection(session, row, match[0].length - 1); + + return this.openingBracketBlock(session, "{", row, match.index); + } + + var match = this.foldingStopMarker.exec(line); + if (match) { + if (match[1]) + return this.latexBlock(session, row, match[0].length - 1); + + return this.closingBracketBlock(session, "}", row, match.index + match[0].length); + } + }; + + this.latexBlock = function(session, row, column) { + var keywords = { + "\\begin": 1, + "\\end": -1 + }; + + var stream = new TokenIterator(session, row, column); + var token = stream.getCurrentToken(); + if (!token || !(token.type == "storage.type" || token.type == "constant.character.escape")) + return; + + var val = token.value; + var dir = keywords[val]; + + var getType = function() { + var token = stream.stepForward(); + var type = token.type == "lparen" ?stream.stepForward().value : ""; + if (dir === -1) { + stream.stepBackward(); + if (type) + stream.stepBackward(); + } + return type; + }; + var stack = [getType()]; + var startColumn = dir === -1 ? stream.getCurrentTokenColumn() : session.getLine(row).length; + var startRow = row; + + stream.step = dir === -1 ? stream.stepBackward : stream.stepForward; + while(token = stream.step()) { + if (!token || !(token.type == "storage.type" || token.type == "constant.character.escape")) + continue; + var level = keywords[token.value]; + if (!level) + continue; + var type = getType(); + if (level === dir) + stack.unshift(type); + else if (stack.shift() !== type || !stack.length) + break; + } + + if (stack.length) + return; + + var row = stream.getCurrentTokenRow(); + if (dir === -1) + return new Range(row, session.getLine(row).length, startRow, startColumn); + stream.stepBackward(); + return new Range(startRow, startColumn, row, stream.getCurrentTokenColumn()); + }; + + this.latexSection = function(session, row, column) { + var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph"]; + + var stream = new TokenIterator(session, row, column); + var token = stream.getCurrentToken(); + if (!token || token.type != "storage.type") + return; + + var startLevel = keywords.indexOf(token.value); + var stackDepth = 0 + var endRow = row; + + while(token = stream.stepForward()) { + if (token.type !== "storage.type") + continue; + var level = keywords.indexOf(token.value); + + if (level >= 2) { + if (!stackDepth) + endRow = stream.getCurrentTokenRow() - 1; + stackDepth += level == 2 ? 1 : - 1; + if (stackDepth < 0) + break + } else if (level >= startLevel) + break; + } + + if (!stackDepth) + endRow = stream.getCurrentTokenRow() - 1; + + while (endRow > row && !/\S/.test(session.getLine(endRow))) + endRow--; + + return new Range( + row, session.getLine(row).length, + endRow, session.getLine(endRow).length + ); + }; + +}).call(FoldMode.prototype); + +}); + +ace.define("ace/mode/latex_beta",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/latex_highlight_rules","ace/mode/folding/latex","ace/range","ace/worker/worker_client"], function(require, exports, module) { +"use strict"; + +var oop = require("../lib/oop"); +var TextMode = require("./text").Mode; +var LatexHighlightRules = require("./latex_highlight_rules").LatexHighlightRules; +var LatexFoldMode = require("./folding/latex").FoldMode; +var Range = require("../range").Range; +var WorkerClient = require("ace/worker/worker_client").WorkerClient; + +var createLatexWorker = function (session) { + var doc = session.getDocument(); + var selection = session.getSelection(); + var cursorAnchor = selection.lead; + + var savedRange = {}; + var suppressions = []; + var hints = []; + var changeHandler = null; + var docChangePending = false; + var firstPass = true; + + var worker = new WorkerClient(["ace"], "ace/mode/latex_beta_worker", "LatexWorker"); + worker.attachToDocument(doc); + var docChangeHandler = doc.on("change", function () { + docChangePending = true; + if(changeHandler) { + clearTimeout(changeHandler); + changeHandler = null; + } + }); + + var cursorHandler = selection.on("changeCursor", function () { + if (docChangePending) { return; } ; + changeHandler = setTimeout(function () { + updateMarkers({cursorMoveOnly:true}); + suppressions = []; + changeHandler = null; + }, 100); + }); + + var updateMarkers = function (options) { + if (!options) { options = {};}; + var cursorMoveOnly = options.cursorMoveOnly; + var annotations = []; + var newRange = {}; + var cursor = selection.getCursor(); + var maxRow = session.getLength() - 1; + var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0; + var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol); + + suppressions = []; + + for (var i = 0, len = hints.length; i 0) { + var originalAnnotations = session.getAnnotations(); + session.setAnnotations(originalAnnotations.concat(annotations)); + }; + firstPass = false; + } else { + session.setAnnotations(annotations); + } + }; + + }; + worker.on("lint", function(results) { + if(docChangePending) { docChangePending = false; }; + hints = results.data; + if (hints.length > 100) { + hints = hints.slice(0, 100); // limit to 100 errors + }; + updateMarkers(); + }); + worker.on("terminate", function() { + if(changeHandler) { + clearTimeout(changeHandler); + changeHandler = null; + } + doc.off("change", docChangeHandler); + selection.off("changeCursor", cursorHandler); + for (var key in savedRange) { + var range = savedRange[key]; + if (range.start !== cursorAnchor) { range.start.detach(); } + if (range.end !== cursorAnchor) { range.end.detach(); } + session.removeMarker(range.id); + } + savedRange = {}; + hints = []; + suppressions = []; + session.clearAnnotations(); + }); + + return worker; +}; + +var Mode = function() { + this.HighlightRules = LatexHighlightRules; + this.foldingRules = new LatexFoldMode(); + this.createWorker = createLatexWorker; +}; +oop.inherits(Mode, TextMode); + +(function() { + this.type = "text"; + + this.lineCommentStart = "%"; + + this.$id = "ace/mode/latex_beta"; +}).call(Mode.prototype); + +exports.Mode = Mode; + +}); diff --git a/services/web/public/js/ace-1.2.5/snippets/latex_beta.js b/services/web/public/js/ace-1.2.5/snippets/latex_beta.js new file mode 100644 index 0000000000..209a682be8 --- /dev/null +++ b/services/web/public/js/ace-1.2.5/snippets/latex_beta.js @@ -0,0 +1,7 @@ +ace.define("ace/snippets/latex_beta",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.snippetText =undefined; +exports.scope = "latex"; + +}); diff --git a/services/web/public/js/ace-1.2.5/worker-latex_beta.js b/services/web/public/js/ace-1.2.5/worker-latex_beta.js new file mode 100644 index 0000000000..b47d8f0a46 --- /dev/null +++ b/services/web/public/js/ace-1.2.5/worker-latex_beta.js @@ -0,0 +1,3052 @@ +"no use strict"; +;(function(window) { +if (typeof window.window != "undefined" && window.document) + return; +if (window.require && window.define) + return; + +if (!window.console) { + window.console = function() { + var msgs = Array.prototype.slice.call(arguments, 0); + postMessage({type: "log", data: msgs}); + }; + window.console.error = + window.console.warn = + window.console.log = + window.console.trace = window.console; +} +window.window = window; +window.ace = window; + +window.onerror = function(message, file, line, col, err) { + postMessage({type: "error", data: { + message: message, + data: err.data, + file: file, + line: line, + col: col, + stack: err.stack + }}); +}; + +window.normalizeModule = function(parentId, moduleName) { + // normalize plugin requires + if (moduleName.indexOf("!") !== -1) { + var chunks = moduleName.split("!"); + return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]); + } + // normalize relative requires + if (moduleName.charAt(0) == ".") { + var base = parentId.split("/").slice(0, -1).join("/"); + moduleName = (base ? base + "/" : "") + moduleName; + + while (moduleName.indexOf(".") !== -1 && previous != moduleName) { + var previous = moduleName; + moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); + } + } + + return moduleName; +}; + +window.require = function require(parentId, id) { + if (!id) { + id = parentId; + parentId = null; + } + if (!id.charAt) + throw new Error("worker.js require() accepts only (parentId, id) as arguments"); + + id = window.normalizeModule(parentId, id); + + var module = window.require.modules[id]; + if (module) { + if (!module.initialized) { + module.initialized = true; + module.exports = module.factory().exports; + } + return module.exports; + } + + if (!window.require.tlns) + return console.log("unable to load " + id); + + var path = resolveModuleId(id, window.require.tlns); + if (path.slice(-3) != ".js") path += ".js"; + + window.require.id = id; + window.require.modules[id] = {}; // prevent infinite loop on broken modules + importScripts(path); + return window.require(parentId, id); +}; +function resolveModuleId(id, paths) { + var testPath = id, tail = ""; + while (testPath) { + var alias = paths[testPath]; + if (typeof alias == "string") { + return alias + tail; + } else if (alias) { + return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name); + } else if (alias === false) { + return ""; + } + var i = testPath.lastIndexOf("/"); + if (i === -1) break; + tail = testPath.substr(i) + tail; + testPath = testPath.slice(0, i); + } + return id; +} +window.require.modules = {}; +window.require.tlns = {}; + +window.define = function(id, deps, factory) { + if (arguments.length == 2) { + factory = deps; + if (typeof id != "string") { + deps = id; + id = window.require.id; + } + } else if (arguments.length == 1) { + factory = id; + deps = []; + id = window.require.id; + } + + if (typeof factory != "function") { + window.require.modules[id] = { + exports: factory, + initialized: true + }; + return; + } + + if (!deps.length) + // If there is no dependencies, we inject "require", "exports" and + // "module" as dependencies, to provide CommonJS compatibility. + deps = ["require", "exports", "module"]; + + var req = function(childId) { + return window.require(id, childId); + }; + + window.require.modules[id] = { + exports: {}, + factory: function() { + var module = this; + var returnExports = factory.apply(this, deps.map(function(dep) { + switch (dep) { + // Because "require", "exports" and "module" aren't actual + // dependencies, we must handle them seperately. + case "require": return req; + case "exports": return module.exports; + case "module": return module; + // But for all other dependencies, we can just go ahead and + // require them. + default: return req(dep); + } + })); + if (returnExports) + module.exports = returnExports; + return module; + } + }; +}; +window.define.amd = {}; +require.tlns = {}; +window.initBaseUrls = function initBaseUrls(topLevelNamespaces) { + for (var i in topLevelNamespaces) + require.tlns[i] = topLevelNamespaces[i]; +}; + +window.initSender = function initSender() { + + var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; + var oop = window.require("ace/lib/oop"); + + var Sender = function() {}; + + (function() { + + oop.implement(this, EventEmitter); + + this.callback = function(data, callbackId) { + postMessage({ + type: "call", + id: callbackId, + data: data + }); + }; + + this.emit = function(name, data) { + postMessage({ + type: "event", + name: name, + data: data + }); + }; + + }).call(Sender.prototype); + + return new Sender(); +}; + +var main = window.main = null; +var sender = window.sender = null; + +window.onmessage = function(e) { + var msg = e.data; + if (msg.event && sender) { + sender._signal(msg.event, msg.data); + } + else if (msg.command) { + if (main[msg.command]) + main[msg.command].apply(main, msg.args); + else if (window[msg.command]) + window[msg.command].apply(window, msg.args); + else + throw new Error("Unknown command:" + msg.command); + } + else if (msg.init) { + window.initBaseUrls(msg.tlns); + require("ace/lib/es5-shim"); + sender = window.sender = window.initSender(); + var clazz = require(msg.module)[msg.classname]; + main = window.main = new clazz(sender); + } +}; +})(this); + +ace.define("ace/lib/oop",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +exports.mixin = function(obj, mixin) { + for (var key in mixin) { + obj[key] = mixin[key]; + } + return obj; +}; + +exports.implement = function(proto, mixin) { + exports.mixin(proto, mixin); +}; + +}); + +ace.define("ace/range",["require","exports","module"], function(require, exports, module) { +"use strict"; +var comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; +var Range = function(startRow, startColumn, endRow, endColumn) { + this.start = { + row: startRow, + column: startColumn + }; + + this.end = { + row: endRow, + column: endColumn + }; +}; + +(function() { + this.isEqual = function(range) { + return this.start.row === range.start.row && + this.end.row === range.end.row && + this.start.column === range.start.column && + this.end.column === range.end.column; + }; + this.toString = function() { + return ("Range: [" + this.start.row + "/" + this.start.column + + "] -> [" + this.end.row + "/" + this.end.column + "]"); + }; + + this.contains = function(row, column) { + return this.compare(row, column) == 0; + }; + this.compareRange = function(range) { + var cmp, + end = range.end, + start = range.start; + + cmp = this.compare(end.row, end.column); + if (cmp == 1) { + cmp = this.compare(start.row, start.column); + if (cmp == 1) { + return 2; + } else if (cmp == 0) { + return 1; + } else { + return 0; + } + } else if (cmp == -1) { + return -2; + } else { + cmp = this.compare(start.row, start.column); + if (cmp == -1) { + return -1; + } else if (cmp == 1) { + return 42; + } else { + return 0; + } + } + }; + this.comparePoint = function(p) { + return this.compare(p.row, p.column); + }; + this.containsRange = function(range) { + return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; + }; + this.intersects = function(range) { + var cmp = this.compareRange(range); + return (cmp == -1 || cmp == 0 || cmp == 1); + }; + this.isEnd = function(row, column) { + return this.end.row == row && this.end.column == column; + }; + this.isStart = function(row, column) { + return this.start.row == row && this.start.column == column; + }; + this.setStart = function(row, column) { + if (typeof row == "object") { + this.start.column = row.column; + this.start.row = row.row; + } else { + this.start.row = row; + this.start.column = column; + } + }; + this.setEnd = function(row, column) { + if (typeof row == "object") { + this.end.column = row.column; + this.end.row = row.row; + } else { + this.end.row = row; + this.end.column = column; + } + }; + this.inside = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column) || this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideStart = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideEnd = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.compare = function(row, column) { + if (!this.isMultiLine()) { + if (row === this.start.row) { + return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); + } + } + + if (row < this.start.row) + return -1; + + if (row > this.end.row) + return 1; + + if (this.start.row === row) + return column >= this.start.column ? 0 : -1; + + if (this.end.row === row) + return column <= this.end.column ? 0 : 1; + + return 0; + }; + this.compareStart = function(row, column) { + if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.compareEnd = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else { + return this.compare(row, column); + } + }; + this.compareInside = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.clipRows = function(firstRow, lastRow) { + if (this.end.row > lastRow) + var end = {row: lastRow + 1, column: 0}; + else if (this.end.row < firstRow) + var end = {row: firstRow, column: 0}; + + if (this.start.row > lastRow) + var start = {row: lastRow + 1, column: 0}; + else if (this.start.row < firstRow) + var start = {row: firstRow, column: 0}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + this.extend = function(row, column) { + var cmp = this.compare(row, column); + + if (cmp == 0) + return this; + else if (cmp == -1) + var start = {row: row, column: column}; + else + var end = {row: row, column: column}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + + this.isEmpty = function() { + return (this.start.row === this.end.row && this.start.column === this.end.column); + }; + this.isMultiLine = function() { + return (this.start.row !== this.end.row); + }; + this.clone = function() { + return Range.fromPoints(this.start, this.end); + }; + this.collapseRows = function() { + if (this.end.column == 0) + return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) + else + return new Range(this.start.row, 0, this.end.row, 0) + }; + this.toScreenRange = function(session) { + var screenPosStart = session.documentToScreenPosition(this.start); + var screenPosEnd = session.documentToScreenPosition(this.end); + + return new Range( + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column + ); + }; + this.moveBy = function(row, column) { + this.start.row += row; + this.start.column += column; + this.end.row += row; + this.end.column += column; + }; + +}).call(Range.prototype); +Range.fromPoints = function(start, end) { + return new Range(start.row, start.column, end.row, end.column); +}; +Range.comparePoints = comparePoints; + +Range.comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; + + +exports.Range = Range; +}); + +ace.define("ace/apply_delta",["require","exports","module"], function(require, exports, module) { +"use strict"; + +function throwDeltaError(delta, errorText){ + console.log("Invalid Delta:", delta); + throw "Invalid Delta: " + errorText; +} + +function positionInDocument(docLines, position) { + return position.row >= 0 && position.row < docLines.length && + position.column >= 0 && position.column <= docLines[position.row].length; +} + +function validateDelta(docLines, delta) { + if (delta.action != "insert" && delta.action != "remove") + throwDeltaError(delta, "delta.action must be 'insert' or 'remove'"); + if (!(delta.lines instanceof Array)) + throwDeltaError(delta, "delta.lines must be an Array"); + if (!delta.start || !delta.end) + throwDeltaError(delta, "delta.start/end must be an present"); + var start = delta.start; + if (!positionInDocument(docLines, delta.start)) + throwDeltaError(delta, "delta.start must be contained in document"); + var end = delta.end; + if (delta.action == "remove" && !positionInDocument(docLines, end)) + throwDeltaError(delta, "delta.end must contained in document for 'remove' actions"); + var numRangeRows = end.row - start.row; + var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); + if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) + throwDeltaError(delta, "delta.range must match delta lines"); +} + +exports.applyDelta = function(docLines, delta, doNotValidate) { + + var row = delta.start.row; + var startColumn = delta.start.column; + var line = docLines[row] || ""; + switch (delta.action) { + case "insert": + var lines = delta.lines; + if (lines.length === 1) { + docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); + } else { + var args = [row, 1].concat(delta.lines); + docLines.splice.apply(docLines, args); + docLines[row] = line.substring(0, startColumn) + docLines[row]; + docLines[row + delta.lines.length - 1] += line.substring(startColumn); + } + break; + case "remove": + var endColumn = delta.end.column; + var endRow = delta.end.row; + if (row === endRow) { + docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); + } else { + docLines.splice( + row, endRow - row + 1, + line.substring(0, startColumn) + docLines[endRow].substring(endColumn) + ); + } + break; + } +} +}); + +ace.define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) { +"use strict"; + +var EventEmitter = {}; +var stopPropagation = function() { this.propagationStopped = true; }; +var preventDefault = function() { this.defaultPrevented = true; }; + +EventEmitter._emit = +EventEmitter._dispatchEvent = function(eventName, e) { + this._eventRegistry || (this._eventRegistry = {}); + this._defaultHandlers || (this._defaultHandlers = {}); + + var listeners = this._eventRegistry[eventName] || []; + var defaultHandler = this._defaultHandlers[eventName]; + if (!listeners.length && !defaultHandler) + return; + + if (typeof e != "object" || !e) + e = {}; + + if (!e.type) + e.type = eventName; + if (!e.stopPropagation) + e.stopPropagation = stopPropagation; + if (!e.preventDefault) + e.preventDefault = preventDefault; + + listeners = listeners.slice(); + for (var i=0; i this.row) + return; + + var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); + this.setPosition(point.row, point.column, true); + }; + + function $pointsInOrder(point1, point2, equalPointsInOrder) { + var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; + return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); + } + + function $getTransformedPoint(delta, point, moveIfEqual) { + var deltaIsInsert = delta.action == "insert"; + var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); + var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column); + var deltaStart = delta.start; + var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range. + if ($pointsInOrder(point, deltaStart, moveIfEqual)) { + return { + row: point.row, + column: point.column + }; + } + if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { + return { + row: point.row + deltaRowShift, + column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) + }; + } + + return { + row: deltaStart.row, + column: deltaStart.column + }; + } + this.setPosition = function(row, column, noClip) { + var pos; + if (noClip) { + pos = { + row: row, + column: column + }; + } else { + pos = this.$clipPositionToDocument(row, column); + } + + if (this.row == pos.row && this.column == pos.column) + return; + + var old = { + row: this.row, + column: this.column + }; + + this.row = pos.row; + this.column = pos.column; + this._signal("change", { + old: old, + value: pos + }); + }; + this.detach = function() { + this.document.removeEventListener("change", this.$onChange); + }; + this.attach = function(doc) { + this.document = doc || this.document; + this.document.on("change", this.$onChange); + }; + this.$clipPositionToDocument = function(row, column) { + var pos = {}; + + if (row >= this.document.getLength()) { + pos.row = Math.max(0, this.document.getLength() - 1); + pos.column = this.document.getLine(pos.row).length; + } + else if (row < 0) { + pos.row = 0; + pos.column = 0; + } + else { + pos.row = row; + pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); + } + + if (column < 0) + pos.column = 0; + + return pos; + }; + +}).call(Anchor.prototype); + +}); + +ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var applyDelta = require("./apply_delta").applyDelta; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Range = require("./range").Range; +var Anchor = require("./anchor").Anchor; + +var Document = function(textOrLines) { + this.$lines = [""]; + if (textOrLines.length === 0) { + this.$lines = [""]; + } else if (Array.isArray(textOrLines)) { + this.insertMergedLines({row: 0, column: 0}, textOrLines); + } else { + this.insert({row: 0, column:0}, textOrLines); + } +}; + +(function() { + + oop.implement(this, EventEmitter); + this.setValue = function(text) { + var len = this.getLength() - 1; + this.remove(new Range(0, 0, len, this.getLine(len).length)); + this.insert({row: 0, column: 0}, text); + }; + this.getValue = function() { + return this.getAllLines().join(this.getNewLineCharacter()); + }; + this.createAnchor = function(row, column) { + return new Anchor(this, row, column); + }; + if ("aaa".split(/a/).length === 0) { + this.$split = function(text) { + return text.replace(/\r\n|\r/g, "\n").split("\n"); + }; + } else { + this.$split = function(text) { + return text.split(/\r\n|\r|\n/); + }; + } + + + this.$detectNewLine = function(text) { + var match = text.match(/^.*?(\r\n|\r|\n)/m); + this.$autoNewLine = match ? match[1] : "\n"; + this._signal("changeNewLineMode"); + }; + this.getNewLineCharacter = function() { + switch (this.$newLineMode) { + case "windows": + return "\r\n"; + case "unix": + return "\n"; + default: + return this.$autoNewLine || "\n"; + } + }; + + this.$autoNewLine = ""; + this.$newLineMode = "auto"; + this.setNewLineMode = function(newLineMode) { + if (this.$newLineMode === newLineMode) + return; + + this.$newLineMode = newLineMode; + this._signal("changeNewLineMode"); + }; + this.getNewLineMode = function() { + return this.$newLineMode; + }; + this.isNewLine = function(text) { + return (text == "\r\n" || text == "\r" || text == "\n"); + }; + this.getLine = function(row) { + return this.$lines[row] || ""; + }; + this.getLines = function(firstRow, lastRow) { + return this.$lines.slice(firstRow, lastRow + 1); + }; + this.getAllLines = function() { + return this.getLines(0, this.getLength()); + }; + this.getLength = function() { + return this.$lines.length; + }; + this.getTextRange = function(range) { + return this.getLinesForRange(range).join(this.getNewLineCharacter()); + }; + this.getLinesForRange = function(range) { + var lines; + if (range.start.row === range.end.row) { + lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; + } else { + lines = this.getLines(range.start.row, range.end.row); + lines[0] = (lines[0] || "").substring(range.start.column); + var l = lines.length - 1; + if (range.end.row - range.start.row == l) + lines[l] = lines[l].substring(0, range.end.column); + } + return lines; + }; + this.insertLines = function(row, lines) { + console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."); + return this.insertFullLines(row, lines); + }; + this.removeLines = function(firstRow, lastRow) { + console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."); + return this.removeFullLines(firstRow, lastRow); + }; + this.insertNewLine = function(position) { + console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."); + return this.insertMergedLines(position, ["", ""]); + }; + this.insert = function(position, text) { + if (this.getLength() <= 1) + this.$detectNewLine(text); + + return this.insertMergedLines(position, this.$split(text)); + }; + this.insertInLine = function(position, text) { + var start = this.clippedPos(position.row, position.column); + var end = this.pos(position.row, position.column + text.length); + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: [text] + }, true); + + return this.clonePos(end); + }; + + this.clippedPos = function(row, column) { + var length = this.getLength(); + if (row === undefined) { + row = length; + } else if (row < 0) { + row = 0; + } else if (row >= length) { + row = length - 1; + column = undefined; + } + var line = this.getLine(row); + if (column == undefined) + column = line.length; + column = Math.min(Math.max(column, 0), line.length); + return {row: row, column: column}; + }; + + this.clonePos = function(pos) { + return {row: pos.row, column: pos.column}; + }; + + this.pos = function(row, column) { + return {row: row, column: column}; + }; + + this.$clipPosition = function(position) { + var length = this.getLength(); + if (position.row >= length) { + position.row = Math.max(0, length - 1); + position.column = this.getLine(length - 1).length; + } else { + position.row = Math.max(0, position.row); + position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); + } + return position; + }; + this.insertFullLines = function(row, lines) { + row = Math.min(Math.max(row, 0), this.getLength()); + var column = 0; + if (row < this.getLength()) { + lines = lines.concat([""]); + column = 0; + } else { + lines = [""].concat(lines); + row--; + column = this.$lines[row].length; + } + this.insertMergedLines({row: row, column: column}, lines); + }; + this.insertMergedLines = function(position, lines) { + var start = this.clippedPos(position.row, position.column); + var end = { + row: start.row + lines.length - 1, + column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length + }; + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: lines + }); + + return this.clonePos(end); + }; + this.remove = function(range) { + var start = this.clippedPos(range.start.row, range.start.column); + var end = this.clippedPos(range.end.row, range.end.column); + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }); + return this.clonePos(start); + }; + this.removeInLine = function(row, startColumn, endColumn) { + var start = this.clippedPos(row, startColumn); + var end = this.clippedPos(row, endColumn); + + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }, true); + + return this.clonePos(start); + }; + this.removeFullLines = function(firstRow, lastRow) { + firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); + lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); + var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; + var deleteLastNewLine = lastRow < this.getLength() - 1; + var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); + var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); + var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var range = new Range(startRow, startCol, endRow, endCol); + var deletedLines = this.$lines.slice(firstRow, lastRow + 1); + + this.applyDelta({ + start: range.start, + end: range.end, + action: "remove", + lines: this.getLinesForRange(range) + }); + return deletedLines; + }; + this.removeNewLine = function(row) { + if (row < this.getLength() - 1 && row >= 0) { + this.applyDelta({ + start: this.pos(row, this.getLine(row).length), + end: this.pos(row + 1, 0), + action: "remove", + lines: ["", ""] + }); + } + }; + this.replace = function(range, text) { + if (!(range instanceof Range)) + range = Range.fromPoints(range.start, range.end); + if (text.length === 0 && range.isEmpty()) + return range.start; + if (text == this.getTextRange(range)) + return range.end; + + this.remove(range); + var end; + if (text) { + end = this.insert(range.start, text); + } + else { + end = range.start; + } + + return end; + }; + this.applyDeltas = function(deltas) { + for (var i=0; i=0; i--) { + this.revertDelta(deltas[i]); + } + }; + this.applyDelta = function(delta, doNotValidate) { + var isInsert = delta.action == "insert"; + if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] + : !Range.comparePoints(delta.start, delta.end)) { + return; + } + + if (isInsert && delta.lines.length > 20000) + this.$splitAndapplyLargeDelta(delta, 20000); + applyDelta(this.$lines, delta, doNotValidate); + this._signal("change", delta); + }; + + this.$splitAndapplyLargeDelta = function(delta, MAX) { + var lines = delta.lines; + var l = lines.length; + var row = delta.start.row; + var column = delta.start.column; + var from = 0, to = 0; + do { + from = to; + to += MAX - 1; + var chunk = lines.slice(from, to); + if (to > l) { + delta.lines = chunk; + delta.start.row = row + from; + delta.start.column = column; + break; + } + chunk.push(""); + this.applyDelta({ + start: this.pos(row + from, column), + end: this.pos(row + to, column = 0), + action: delta.action, + lines: chunk + }, true); + } while(true); + }; + this.revertDelta = function(delta) { + this.applyDelta({ + start: this.clonePos(delta.start), + end: this.clonePos(delta.end), + action: (delta.action == "insert" ? "remove" : "insert"), + lines: delta.lines.slice() + }); + }; + this.indexToPosition = function(index, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + for (var i = startRow || 0, l = lines.length; i < l; i++) { + index -= lines[i].length + newlineLength; + if (index < 0) + return {row: i, column: index + lines[i].length + newlineLength}; + } + return {row: l-1, column: lines[l-1].length}; + }; + this.positionToIndex = function(pos, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + var index = 0; + var row = Math.min(pos.row, lines.length); + for (var i = startRow || 0; i < row; ++i) + index += lines[i].length + newlineLength; + + return index + pos.column; + }; + +}).call(Document.prototype); + +exports.Document = Document; +}); + +ace.define("ace/lib/lang",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.last = function(a) { + return a[a.length - 1]; +}; + +exports.stringReverse = function(string) { + return string.split("").reverse().join(""); +}; + +exports.stringRepeat = function (string, count) { + var result = ''; + while (count > 0) { + if (count & 1) + result += string; + + if (count >>= 1) + string += string; + } + return result; +}; + +var trimBeginRegexp = /^\s\s*/; +var trimEndRegexp = /\s\s*$/; + +exports.stringTrimLeft = function (string) { + return string.replace(trimBeginRegexp, ''); +}; + +exports.stringTrimRight = function (string) { + return string.replace(trimEndRegexp, ''); +}; + +exports.copyObject = function(obj) { + var copy = {}; + for (var key in obj) { + copy[key] = obj[key]; + } + return copy; +}; + +exports.copyArray = function(array){ + var copy = []; + for (var i=0, l=array.length; i MAX_TOKENS) { + throw new Error("exceed max token count of " + MAX_TOKENS); + break; + }; + var result = SPECIAL.exec(text); + if (result == null) { + if (idx < text.length) { + Tokens.push([lineNumber, "Text", idx, text.length]); + } + break; + } + if (result && result.index <= pos) { + throw new Error("infinite loop in parsing"); + break; + }; + pos = result.index; + if (pos > idx) { + Tokens.push([lineNumber, "Text", idx, pos]); + } + for (var i = idx; i < pos; i++) { + if (text[i] === "\n") { + lineNumber++; + linePosition[lineNumber] = i+1; + } + } + + var newIdx = SPECIAL.lastIndex; + idx = newIdx; + var code = result[0]; + if (code === "%") { // comment character + var newLinePos = text.indexOf("\n", idx); + if (newLinePos === -1) { + newLinePos = text.length; + }; + var commentString = text.substring(idx, newLinePos); + if (commentString.indexOf("%novalidate") === 0) { + return []; + } else if(!checkingDisabled && commentString.indexOf("%begin novalidate") === 0) { + checkingDisabled = true; + } else if (checkingDisabled && commentString.indexOf("%end novalidate") === 0) { + checkingDisabled = false; + }; + idx = SPECIAL.lastIndex = newLinePos + 1; + Comments.push([lineNumber, idx, newLinePos]); + lineNumber++; + linePosition[lineNumber] = idx; + } else if (checkingDisabled) { + continue; + } else if (code === '\\') { // escape character + NEXTCS.lastIndex = idx; + var controlSequence = NEXTCS.exec(text); + var nextSpecialPos = controlSequence === null ? idx : controlSequence.index; + if (nextSpecialPos === idx) { + Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]); + idx = SPECIAL.lastIndex = idx + 1; + char = text[nextSpecialPos]; + if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;}; + } else { + Tokens.push([lineNumber, code, pos, nextSpecialPos, text.slice(idx, nextSpecialPos)]); + var char; + while ((char = text[nextSpecialPos]) === ' ' || char === '\t' || char === '\r' || char === '\n') { + nextSpecialPos++; + if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;}; + } + idx = SPECIAL.lastIndex = nextSpecialPos; + } + } else if (code === "{") { // open group + Tokens.push([lineNumber, code, pos]); + } else if (code === "}") { // close group + Tokens.push([lineNumber, code, pos]); + } else if (code === "$") { // math mode + Tokens.push([lineNumber, code, pos]); + } else if (code === "&") { // tabalign + Tokens.push([lineNumber, code, pos]); + } else if (code === "#") { // macro parameter + Tokens.push([lineNumber, code, pos]); + } else if (code === "^") { // superscript + Tokens.push([lineNumber, code, pos]); + } else if (code === "_") { // subscript + Tokens.push([lineNumber, code, pos]); + } else if (code === "~") { // active character (space) + Tokens.push([lineNumber, code, pos]); + } else { + throw "unrecognised character " + code; + } + } + + return {tokens: Tokens, comments: Comments, linePosition: linePosition, lineNumber: lineNumber, text: text}; +}; + +var read1arg = function (TokeniseResult, k, options) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + if (options && options.allowStar) { + var optional = Tokens[k+1]; + if (optional && optional[1] === "Text") { + var optionalstr = text.substring(optional[2], optional[3]); + if (optionalstr === "*") { k++;} + }; + }; + + var open = Tokens[k+1]; + var env = Tokens[k+2]; + var close = Tokens[k+3]; + var envName; + + if(open && open[1] === "\\") { + envName = open[4]; // array element 4 is command sequence + return k + 1; + } else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") { + envName = env[4]; // NOTE: if we were actually using this, keep track of * above + return k + 3; // array element 4 is command sequence + } else { + return null; + } +}; + + +var read1name = function (TokeniseResult, k) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var open = Tokens[k+1]; + var env = Tokens[k+2]; + var close = Tokens[k+3]; + + if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") { + var envName = text.substring(env[2], env[3]); + return k + 3; + } else if (open && open[1] === "{" && env && env[1] === "Text") { + envName = ""; + for (var j = k + 2, tok; (tok = Tokens[j]); j++) { + if (tok[1] === "Text") { + var str = text.substring(tok[2], tok[3]); + if (!str.match(/^\S*$/)) { break; } + envName = envName + str; + } else if (tok[1] === "_") { + envName = envName + "_"; + } else { + break; + } + } + if (tok && tok[1] === "}") { + return j; // advance past these tokens + } else { + return null; + } + } else { + return null; + } +}; + +var read1filename = function (TokeniseResult, k) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var fileName = ""; + for (var j = k + 1, tok; (tok = Tokens[j]); j++) { + if (tok[1] === "Text") { + var str = text.substring(tok[2], tok[3]); + if (!str.match(/^\S*$/)) { break; } + fileName = fileName + str; + } else if (tok[1] === "_") { + fileName = fileName + "_"; + } else { + break; + } + } + if (fileName.length > 0) { + return j; // advance past these tokens + } else { + return null; + } +}; + +var readOptionalParams = function(TokeniseResult, k) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var params = Tokens[k+1]; + + if(params && params[1] === "Text") { + var paramNum = text.substring(params[2], params[3]); + if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) { + return k + 1; // got it + }; + }; + return null; +}; + +var readDefinition = function(TokeniseResult, k) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + k = k + 1; + var count = 0; + var nextToken = Tokens[k]; + while (nextToken && nextToken[1] === "Text") { + var start = nextToken[2], end = nextToken[3]; + for (var i = start; i < end; i++) { + var char = text[i]; + if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { continue; } + return null; // bail out, should begin with a { + } + k++; + nextToken = Tokens[k]; + } + if (nextToken && nextToken[1] === "{") { + count++; + while (count>0) { + k++; + nextToken = Tokens[k]; + if(!nextToken) { break; }; + if (nextToken[1] === "}") { count--; } + if (nextToken[1] === "{") { count++; } + } + return k; + } + + return null; +}; + +var readVerb = function(TokeniseResult, k) { + + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var verbToken = Tokens[k]; + var verbStr = text.substring(verbToken[2], verbToken[3]); + var pos = verbToken[3]; + if (text[pos] === "*") { pos++; } // \verb* form of command + var delimiter = text[pos]; + pos++; + + var nextToken = Tokens[k+1]; + for (var i = pos, end = text.length; i < end; i++) { + var char = text[i]; + if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; + if (char === delimiter) { return k; }; + if (char === '\r' || char === '\n') { return null; } + }; + + return null; +}; + +var readUrl = function(TokeniseResult, k) { + + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var urlToken = Tokens[k]; + var urlStr = text.substring(urlToken[2], urlToken[3]); + var pos = urlToken[3]; + var openDelimiter = text[pos]; + var closeDelimiter = (openDelimiter === "{") ? "}" : openDelimiter; + var nextToken = Tokens[k+1]; + if (nextToken && pos === nextToken[2]) { + k++; + nextToken = Tokens[k+1]; + }; + pos++; + + var count = 1; + for (var i = pos, end = text.length; count > 0 && i < end; i++) { + var char = text[i]; + if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; + if (char === closeDelimiter) { + count--; + } else if (char === openDelimiter) { + count++; + }; + if (count === 0) { return k; }; + if (char === '\r' || char === '\n') { return null; } + }; + + return null; +}; + +var InterpretTokens = function (TokeniseResult, ErrorReporter) { + var Tokens = TokeniseResult.tokens; + var linePosition = TokeniseResult.linePosition; + var lineNumber = TokeniseResult.lineNumber; + var text = TokeniseResult.text; + + var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo; + var TokenError = ErrorReporter.TokenError; + var Environments = new EnvHandler(ErrorReporter); + + var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox) + + 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 (seq === "left" || seq === "right") { + var nextToken = Tokens[i+1]; + char = ""; + if (nextToken && nextToken[1] === "Text") { + char = text.substring(nextToken[2], nextToken[2] + 1); + } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") { + char = nextToken[4]; + } else if (nextToken && nextToken[1] === "\\") { + char = "unknown"; + } + if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) { + TokenError(token, "invalid bracket command"); + } else { + i = i + 1; + Environments.push({command:seq, token:token}); + }; + } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") { + Environments.push({command:seq, token:token}); + } else if (seq === "input") { + newPos = read1filename(TokeniseResult, i); + if (newPos === null) { continue; } else {i = newPos;}; + } else if (seq === "hbox" || seq === "text" || seq === "mbox") { + nextGroupMathMode = false; + } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) { + var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + if (currentMathMode === null && !insideGroup) { + TokenError(token, type + seq + " must be inside math mode"); + }; + } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) { + currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + if (currentMathMode && !insideGroup) { + TokenError(token, type + seq + " used inside math mode"); + Environments.resetMathMode(); + }; + }; + } else if (type === "{") { + Environments.push({command:"{", token:token, mathMode: nextGroupMathMode}); + nextGroupMathMode = null; + } else if (type === "}") { + Environments.push({command:"}", token:token}); + } else if (type === "$") { + var lookAhead = Tokens[i+1]; + var nextIsDollar = lookAhead && lookAhead[1] === "$"; + currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) { + Environments.push({command:"$$", token:token}); + i = i + 1; + } else { + Environments.push({command:"$", token:token}); + } + } else if (type === "^" || type === "_") { + currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + var insideGroup = Environments.insideGroup(); // true if inside {....} + if (currentMathMode === null && !insideGroup) { + TokenError(token, type + " must be inside math mode"); + }; + } else { + nextGroupMathMode = null; + } + }; + return Environments; +}; + +var EnvHandler = function (ErrorReporter) { + var ErrorTo = ErrorReporter.EnvErrorTo; + var ErrorFromTo = ErrorReporter.EnvErrorFromTo; + var ErrorFrom = ErrorReporter.EnvErrorFrom; + + var envs = []; + + var state = []; + var documentClosed = null; + var inVerbatim = false; + var verbatimRanges = []; + + this.Environments = envs; + + this.push = function (newEnv) { + this.setEnvProps(newEnv); + this.checkAndUpdateState(newEnv); + envs.push(newEnv); + }; + + this._endVerbatim = function (thisEnv) { + var lastEnv = state.pop(); + if (lastEnv && lastEnv.name === thisEnv.name) { + inVerbatim = false; + verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]}); + } else { + if(lastEnv) { state.push(lastEnv); } ; + } + }; + + var invalidEnvs = []; + + this._end = function (thisEnv) { + do { + var lastEnv = state.pop(); + var retry = false; + var i; + + if (closedBy(lastEnv, thisEnv)) { + if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) { + documentClosed = thisEnv; + }; + return; + } else if (!lastEnv) { + if (documentClosed) { + ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); + } else { + ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}"); + } + } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) { + invalidEnvs.splice(i, 1); + if (lastEnv) { state.push(lastEnv); } ; + return; + } else { + var status = reportError(lastEnv, thisEnv); + if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) { + invalidEnvs.push(lastEnv); + retry = true; + } else { + var prevLastEnv = state.pop(); + if(prevLastEnv) { + if (thisEnv.name === prevLastEnv.name) { + return; + } else { + state.push(prevLastEnv); + } + } + invalidEnvs.push(lastEnv); + } + + } + } while (retry === true); + }; + + var CLOSING_DELIMITER = { + "{" : "}", + "left" : "right", + "[" : "]", + "(" : ")", + "$" : "$", + "$$": "$$" + }; + + var closedBy = function (lastEnv, thisEnv) { + if (!lastEnv) { + return false ; + } else if (thisEnv.command === "end") { + return lastEnv.command === "begin" && lastEnv.name === thisEnv.name; + } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) { + return true; + } else { + return false; + } + }; + + var indexOfClosingEnvInArray = function (envs, thisEnv) { + for (var i = 0, n = envs.length; i < n ; i++) { + if (closedBy(envs[i], thisEnv)) { + return i; + } + } + return -1; + }; + + var envPrecedence = function (env) { + var openScore = { + "{" : 1, + "left" : 2, + "$" : 3, + "$$" : 4, + "begin": 4 + }; + var closeScore = { + "}" : 1, + "right" : 2, + "$" : 3, + "$$" : 5, + "end": 4 + }; + if (env.command) { + return openScore[env.command] || closeScore[env.command]; + } else { + return 0; + } + }; + + var getName = function(env) { + var description = { + "{" : "open group {", + "}" : "close group }", + "[" : "open display math \\[", + "]" : "close display math \\]", + "(" : "open inline math \\(", + ")" : "close inline math \\)", + "$" : "$", + "$$" : "$$", + "left" : "\\left", + "right" : "\\right" + }; + if (env.command === "begin" || env.command === "end") { + return "\\" + env.command + "{" + env.name + "}"; + } else if (env.command in description) { + return description[env.command]; + } else { + return env.command; + } + }; + + var EXTRA_CLOSE = 1; + var UNCLOSED_GROUP = 2; + var UNCLOSED_ENV = 3; + + var reportError = function(lastEnv, thisEnv) { + if (!lastEnv) { // unexpected close, nothing was open! + if (documentClosed) { + ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); + } else { + ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); + }; + return EXTRA_CLOSE; + } else if (lastEnv.command === "{" && thisEnv.command === "end") { + ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), + {suppressIfEditing:true, errorAtStart: true, type:"warning"}); + return UNCLOSED_GROUP; + } else { + var pLast = envPrecedence(lastEnv); + var pThis = envPrecedence(thisEnv); + if (pThis > pLast) { + ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), + {suppressIfEditing:true, errorAtStart: true}); + } else { + ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv)); + } + return UNCLOSED_ENV; + }; + }; + + this._beginMathMode = function (thisEnv) { + var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env + if (currentMathMode) { + ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode), + {suppressIfEditing:true, errorAtStart: true}); + }; + thisEnv.mathMode = thisEnv; + state.push(thisEnv); + }; + + this._toggleMathMode = function (thisEnv) { + var lastEnv = state.pop(); + if (closedBy(lastEnv, thisEnv)) { + return; + } else { + if (lastEnv) {state.push(lastEnv);} + if (lastEnv && lastEnv.mathMode) { + this._end(thisEnv); + } else { + thisEnv.mathMode = thisEnv; + state.push(thisEnv); + } + }; + }; + + this.getMathMode = function () { + var n = state.length; + if (n > 0) { + return state[n-1].mathMode; + } else { + return null; + } + }; + + this.insideGroup = function () { + var n = state.length; + if (n > 0) { + return (state[n-1].command === "{"); + } else { + return null; + } + }; + + var resetMathMode = function () { + var n = state.length; + if (n > 0) { + var lastMathMode = state[n-1].mathMode; + do { + var lastEnv = state.pop(); + } while (lastEnv && lastEnv !== lastMathMode); + } else { + return; + } + }; + + this.resetMathMode = resetMathMode; + + var getNewMathMode = function (currentMathMode, thisEnv) { + var newMathMode = null; + + if (thisEnv.command === "{") { + if (thisEnv.mathMode !== null) { + newMathMode = thisEnv.mathMode; + } else { + newMathMode = currentMathMode; + } + } else if (thisEnv.command === "left") { + if (currentMathMode === null) { + ErrorFrom(thisEnv, "\\left can only be used in math mode"); + }; + newMathMode = currentMathMode; + } else if (thisEnv.command === "begin") { + var name = thisEnv.name; + if (name) { + if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { + if (currentMathMode) { + ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), + {suppressIfEditing:true, errorAtStart: true}); + resetMathMode(); + }; + newMathMode = null; + } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) { + if (!currentMathMode) { + ErrorFrom(thisEnv, thisEnv.name + " not inside math mode"); + }; + newMathMode = currentMathMode; + } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) { + if (currentMathMode) { + ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), + {suppressIfEditing:true, errorAtStart: true}); + resetMathMode(); + }; + newMathMode = thisEnv; + } else { + newMathMode = undefined; // undefined means we don't know if we are in math mode or not + } + } + }; + return newMathMode; + }; + + this.checkAndUpdateState = function (thisEnv) { + if (inVerbatim) { + if (thisEnv.command === "end") { + this._endVerbatim(thisEnv); + } else { + return; // ignore anything in verbatim environments + } + } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") { + if (thisEnv.verbatim) {inVerbatim = true;}; + var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env + var newMathMode = getNewMathMode(currentMathMode, thisEnv); + thisEnv.mathMode = newMathMode; + state.push(thisEnv); + } else if (thisEnv.command === "end") { + this._end(thisEnv); + } else if (thisEnv.command === "(" || thisEnv.command === "[") { + this._beginMathMode(thisEnv); + } else if (thisEnv.command === ")" || thisEnv.command === "]") { + this._end(thisEnv); + } else if (thisEnv.command === "}") { + this._end(thisEnv); + } else if (thisEnv.command === "right") { + this._end(thisEnv); + } else if (thisEnv.command === "$" || thisEnv.command === "$$") { + this._toggleMathMode(thisEnv); + } + }; + + this.close = function () { + while (state.length > 0) { + var thisEnv = state.pop(); + if (thisEnv.command === "{") { + ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); + } else { + ErrorFrom(thisEnv, "unclosed " + getName(thisEnv)); + } + } + var vlen = verbatimRanges.length; + var len = ErrorReporter.tokenErrors.length; + if (vlen >0 && len > 0) { + for (var i = 0; i < len; i++) { + var tokenError = ErrorReporter.tokenErrors[i]; + var startPos = tokenError.startPos; + var endPos = tokenError.endPos; + for (var j = 0; j < vlen; j++) { + if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) { + tokenError.ignore = true; + break; + } + } + } + } + }; + + this.setEnvProps = function (env) { + var name = env.name ; + if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) { + env.verbatim = true; + } + }; +}; +var ErrorReporter = function (TokeniseResult) { + var text = TokeniseResult.text; + var linePosition = TokeniseResult.linePosition; + var lineNumber = TokeniseResult.lineNumber; + + var errors = [], tokenErrors = []; + this.errors = errors; + this.tokenErrors = tokenErrors; + + this.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, options) { + if(!options) { options = { suppressIfEditing:true } ; }; + var line = token[0], type = token[1], start = token[2], end = token[3]; + var start_col = start - linePosition[line]; + if (!end) { end = start + 1; } ; + var end_col = end - linePosition[line]; + tokenErrors.push({row: line, + column: start_col, + start_row:line, + start_col: start_col, + end_row:line, + end_col: end_col, + type:"error", + text:message, + startPos: start, + endPos: end, + suppressIfEditing:options.suppressIfEditing}); + }; + + this.TokenErrorFromTo = function (fromToken, toToken, message, options) { + if(!options) { options = {suppressIfEditing:true } ; }; + var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3]; + var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3]; + if (!toEnd) { toEnd = toStart + 1;}; + var start_col = fromStart - linePosition[fromLine]; + var end_col = toEnd - linePosition[toLine]; + + tokenErrors.push({row: fromLine, + column: start_col, + start_row: fromLine, + start_col: start_col, + end_row: toLine, + end_col: end_col, + type:"error", + text:message, + startPos: fromStart, + endPos: toEnd, + suppressIfEditing:options.suppressIfEditing}); + }; + + + 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); + Environments.close(); + return Reporter.getErrors(); +}; + +(function() { + var disabled = false; + + this.onUpdate = function() { + if (disabled) { return ; }; + + var value = this.doc.getValue(); + var errors = []; + try { + if (value) + errors = Parse(value); + } catch (e) { + disabled = true; + errors = []; + } + this.sender.emit("lint", errors); + }; + +}).call(LatexWorker.prototype); + +}); + +ace.define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) { + +function Empty() {} + +if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { // .length is 1 + var target = this; + if (typeof target != "function") { + throw new TypeError("Function.prototype.bind called on incompatible " + target); + } + var args = slice.call(arguments, 1); // for normal call + var bound = function () { + + if (this instanceof bound) { + + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + if(target.prototype) { + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + return bound; + }; +} +var call = Function.prototype.call; +var prototypeOfArray = Array.prototype; +var prototypeOfObject = Object.prototype; +var slice = prototypeOfArray.slice; +var _toString = call.bind(prototypeOfObject.toString); +var owns = call.bind(prototypeOfObject.hasOwnProperty); +var defineGetter; +var defineSetter; +var lookupGetter; +var lookupSetter; +var supportsAccessors; +if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) { + defineGetter = call.bind(prototypeOfObject.__defineGetter__); + defineSetter = call.bind(prototypeOfObject.__defineSetter__); + lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); + lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); +} +if ([1,2].splice(0).length != 2) { + if(function() { // test IE < 9 to splice bug - see issue #138 + function makeArray(l) { + var a = new Array(l+2); + a[0] = a[1] = 0; + return a; + } + var array = [], lengthBefore; + + array.splice.apply(array, makeArray(20)); + array.splice.apply(array, makeArray(26)); + + lengthBefore = array.length; //46 + array.splice(5, 0, "XXX"); // add one element + + lengthBefore + 1 == array.length + + if (lengthBefore + 1 == array.length) { + return true;// has right splice implementation without bugs + } + }()) {//IE 6/7 + var array_splice = Array.prototype.splice; + Array.prototype.splice = function(start, deleteCount) { + if (!arguments.length) { + return []; + } else { + return array_splice.apply(this, [ + start === void 0 ? 0 : start, + deleteCount === void 0 ? (this.length - start) : deleteCount + ].concat(slice.call(arguments, 2))) + } + }; + } else {//IE8 + Array.prototype.splice = function(pos, removeCount){ + var length = this.length; + if (pos > 0) { + if (pos > length) + pos = length; + } else if (pos == void 0) { + pos = 0; + } else if (pos < 0) { + pos = Math.max(length + pos, 0); + } + + if (!(pos+removeCount < length)) + removeCount = length - pos; + + var removed = this.slice(pos, pos+removeCount); + var insert = slice.call(arguments, 2); + var add = insert.length; + if (pos === length) { + if (add) { + this.push.apply(this, insert); + } + } else { + var remove = Math.min(removeCount, length - pos); + var tailOldPos = pos + remove; + var tailNewPos = tailOldPos + add - remove; + var tailCount = length - tailOldPos; + var lengthAfterRemove = length - remove; + + if (tailNewPos < tailOldPos) { // case A + for (var i = 0; i < tailCount; ++i) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } else if (tailNewPos > tailOldPos) { // case B + for (i = tailCount; i--; ) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } // else, add == remove (nothing to do) + + if (add && pos === lengthAfterRemove) { + this.length = lengthAfterRemove; // truncate array + this.push.apply(this, insert); + } else { + this.length = lengthAfterRemove + add; // reserves space + for (i = 0; i < add; ++i) { + this[pos+i] = insert[i]; + } + } + } + return removed; + }; + } +} +if (!Array.isArray) { + Array.isArray = function isArray(obj) { + return _toString(obj) == "[object Array]"; + }; +} +var boxedString = Object("a"), + splitString = boxedString[0] != "a" || !(0 in boxedString); + +if (!Array.prototype.forEach) { + Array.prototype.forEach = function forEach(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + thisp = arguments[1], + i = -1, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + while (++i < length) { + if (i in self) { + fun.call(thisp, self[i], i, object); + } + } + }; +} +if (!Array.prototype.map) { + Array.prototype.map = function map(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = Array(length), + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) + result[i] = fun.call(thisp, self[i], i, object); + } + return result; + }; +} +if (!Array.prototype.filter) { + Array.prototype.filter = function filter(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = [], + value, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) { + value = self[i]; + if (fun.call(thisp, value, i, object)) { + result.push(value); + } + } + } + return result; + }; +} +if (!Array.prototype.every) { + Array.prototype.every = function every(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && !fun.call(thisp, self[i], i, object)) { + return false; + } + } + return true; + }; +} +if (!Array.prototype.some) { + Array.prototype.some = function some(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && fun.call(thisp, self[i], i, object)) { + return true; + } + } + return false; + }; +} +if (!Array.prototype.reduce) { + Array.prototype.reduce = function reduce(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduce of empty array with no initial value"); + } + + var i = 0; + var result; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i++]; + break; + } + if (++i >= length) { + throw new TypeError("reduce of empty array with no initial value"); + } + } while (true); + } + + for (; i < length; i++) { + if (i in self) { + result = fun.call(void 0, result, self[i], i, object); + } + } + + return result; + }; +} +if (!Array.prototype.reduceRight) { + Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + + var result, i = length - 1; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i--]; + break; + } + if (--i < 0) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + } while (true); + } + + do { + if (i in this) { + result = fun.call(void 0, result, self[i], i, object); + } + } while (i--); + + return result; + }; +} +if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) { + Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + + var i = 0; + if (arguments.length > 1) { + i = toInteger(arguments[1]); + } + i = i >= 0 ? i : Math.max(0, length + i); + for (; i < length; i++) { + if (i in self && self[i] === sought) { + return i; + } + } + return -1; + }; +} +if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) { + Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + var i = length - 1; + if (arguments.length > 1) { + i = Math.min(i, toInteger(arguments[1])); + } + i = i >= 0 ? i : length - Math.abs(i); + for (; i >= 0; i--) { + if (i in self && sought === self[i]) { + return i; + } + } + return -1; + }; +} +if (!Object.getPrototypeOf) { + Object.getPrototypeOf = function getPrototypeOf(object) { + return object.__proto__ || ( + object.constructor ? + object.constructor.prototype : + prototypeOfObject + ); + }; +} +if (!Object.getOwnPropertyDescriptor) { + var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " + + "non-object: "; + Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT + object); + if (!owns(object, property)) + return; + + var descriptor, getter, setter; + descriptor = { enumerable: true, configurable: true }; + if (supportsAccessors) { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + + var getter = lookupGetter(object, property); + var setter = lookupSetter(object, property); + object.__proto__ = prototype; + + if (getter || setter) { + if (getter) descriptor.get = getter; + if (setter) descriptor.set = setter; + return descriptor; + } + } + descriptor.value = object[property]; + return descriptor; + }; +} +if (!Object.getOwnPropertyNames) { + Object.getOwnPropertyNames = function getOwnPropertyNames(object) { + return Object.keys(object); + }; +} +if (!Object.create) { + var createEmpty; + if (Object.prototype.__proto__ === null) { + createEmpty = function () { + return { "__proto__": null }; + }; + } else { + createEmpty = function () { + var empty = {}; + for (var i in empty) + empty[i] = null; + empty.constructor = + empty.hasOwnProperty = + empty.propertyIsEnumerable = + empty.isPrototypeOf = + empty.toLocaleString = + empty.toString = + empty.valueOf = + empty.__proto__ = null; + return empty; + } + } + + Object.create = function create(prototype, properties) { + var object; + if (prototype === null) { + object = createEmpty(); + } else { + if (typeof prototype != "object") + throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'"); + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (properties !== void 0) + Object.defineProperties(object, properties); + return object; + }; +} + +function doesDefinePropertyWork(object) { + try { + Object.defineProperty(object, "sentinel", {}); + return "sentinel" in object; + } catch (exception) { + } +} +if (Object.defineProperty) { + var definePropertyWorksOnObject = doesDefinePropertyWork({}); + var definePropertyWorksOnDom = typeof document == "undefined" || + doesDefinePropertyWork(document.createElement("div")); + if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { + var definePropertyFallback = Object.defineProperty; + } +} + +if (!Object.defineProperty || definePropertyFallback) { + var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: "; + var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: " + var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " + + "on this javascript engine"; + + Object.defineProperty = function defineProperty(object, property, descriptor) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT_TARGET + object); + if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) + throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); + if (definePropertyFallback) { + try { + return definePropertyFallback.call(Object, object, property, descriptor); + } catch (exception) { + } + } + if (owns(descriptor, "value")) { + + if (supportsAccessors && (lookupGetter(object, property) || + lookupSetter(object, property))) + { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + delete object[property]; + object[property] = descriptor.value; + object.__proto__ = prototype; + } else { + object[property] = descriptor.value; + } + } else { + if (!supportsAccessors) + throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); + if (owns(descriptor, "get")) + defineGetter(object, property, descriptor.get); + if (owns(descriptor, "set")) + defineSetter(object, property, descriptor.set); + } + + return object; + }; +} +if (!Object.defineProperties) { + Object.defineProperties = function defineProperties(object, properties) { + for (var property in properties) { + if (owns(properties, property)) + Object.defineProperty(object, property, properties[property]); + } + return object; + }; +} +if (!Object.seal) { + Object.seal = function seal(object) { + return object; + }; +} +if (!Object.freeze) { + Object.freeze = function freeze(object) { + return object; + }; +} +try { + Object.freeze(function () {}); +} catch (exception) { + Object.freeze = (function freeze(freezeObject) { + return function freeze(object) { + if (typeof object == "function") { + return object; + } else { + return freezeObject(object); + } + }; + })(Object.freeze); +} +if (!Object.preventExtensions) { + Object.preventExtensions = function preventExtensions(object) { + return object; + }; +} +if (!Object.isSealed) { + Object.isSealed = function isSealed(object) { + return false; + }; +} +if (!Object.isFrozen) { + Object.isFrozen = function isFrozen(object) { + return false; + }; +} +if (!Object.isExtensible) { + Object.isExtensible = function isExtensible(object) { + if (Object(object) === object) { + throw new TypeError(); // TODO message + } + var name = ''; + while (owns(object, name)) { + name += '?'; + } + object[name] = true; + var returnValue = owns(object, name); + delete object[name]; + return returnValue; + }; +} +if (!Object.keys) { + var hasDontEnumBug = true, + dontEnums = [ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "constructor" + ], + dontEnumsLength = dontEnums.length; + + for (var key in {"toString": null}) { + hasDontEnumBug = false; + } + + Object.keys = function keys(object) { + + if ( + (typeof object != "object" && typeof object != "function") || + object === null + ) { + throw new TypeError("Object.keys called on a non-object"); + } + + var keys = []; + for (var name in object) { + if (owns(object, name)) { + keys.push(name); + } + } + + if (hasDontEnumBug) { + for (var i = 0, ii = dontEnumsLength; i < ii; i++) { + var dontEnum = dontEnums[i]; + if (owns(object, dontEnum)) { + keys.push(dontEnum); + } + } + } + return keys; + }; + +} +if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; +} +var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; +if (!String.prototype.trim || ws.trim()) { + ws = "[" + ws + "]"; + var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), + trimEndRegexp = new RegExp(ws + ws + "*$"); + String.prototype.trim = function trim() { + return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); + }; +} + +function toInteger(n) { + n = +n; + if (n !== n) { // isNaN + n = 0; + } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + return n; +} + +function isPrimitive(input) { + var type = typeof input; + return ( + input === null || + type === "undefined" || + type === "boolean" || + type === "number" || + type === "string" + ); +} + +function toPrimitive(input) { + var val, valueOf, toString; + if (isPrimitive(input)) { + return input; + } + valueOf = input.valueOf; + if (typeof valueOf === "function") { + val = valueOf.call(input); + if (isPrimitive(val)) { + return val; + } + } + toString = input.toString; + if (typeof toString === "function") { + val = toString.call(input); + if (isPrimitive(val)) { + return val; + } + } + throw new TypeError(); +} +var toObject = function (o) { + if (o == null) { // this matches both null and undefined + throw new TypeError("can't convert "+o+" to object"); + } + return Object(o); +}; + +}); From 2583a2808783d15ed80eb38501739236fd7dc5ee Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 15 Dec 2016 13:28:00 +0000 Subject: [PATCH 12/31] rollout linter math mode to all users ace repo commit 21dde1e5f104b02e19534e56d7eb2dbd3a950db9 --- .../web/public/js/ace-1.2.5/mode-latex.js | 14 +- .../web/public/js/ace-1.2.5/worker-latex.js | 509 ++++++++++++++---- 2 files changed, 415 insertions(+), 108 deletions(-) diff --git a/services/web/public/js/ace-1.2.5/mode-latex.js b/services/web/public/js/ace-1.2.5/mode-latex.js index f183d7c263..8e7bbe4802 100644 --- a/services/web/public/js/ace-1.2.5/mode-latex.js +++ b/services/web/public/js/ace-1.2.5/mode-latex.js @@ -242,6 +242,10 @@ var createLatexWorker = function (session) { var annotations = []; var newRange = {}; var cursor = selection.getCursor(); + var maxRow = session.getLength() - 1; + var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0; + var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol); + suppressions = []; for (var i = 0, len = hints.length; i 0) { + return j; // advance past these tokens + } else { + return null; + } +}; + var readOptionalParams = function(TokeniseResult, k) { var Tokens = TokeniseResult.tokens; var text = TokeniseResult.text; @@ -1697,7 +1715,6 @@ var readUrl = function(TokeniseResult, k) { return null; }; - var InterpretTokens = function (TokeniseResult, ErrorReporter) { var Tokens = TokeniseResult.tokens; var linePosition = TokeniseResult.linePosition; @@ -1706,7 +1723,9 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo; var TokenError = ErrorReporter.TokenError; - var Environments = []; + var Environments = new EnvHandler(ErrorReporter); + + var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox) for (var i = 0, len = Tokens.length; i < len; i++) { var token = Tokens[i]; @@ -1791,128 +1810,407 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { } else if (seq === "url") { newPos = readUrl(TokeniseResult, i); if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;}; - } + } else if (seq === "left" || seq === "right") { + var nextToken = Tokens[i+1]; + char = ""; + if (nextToken && nextToken[1] === "Text") { + char = text.substring(nextToken[2], nextToken[2] + 1); + } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") { + char = nextToken[4]; + } else if (nextToken && nextToken[1] === "\\") { + char = "unknown"; + } + if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) { + TokenError(token, "invalid bracket command"); + } else { + i = i + 1; + Environments.push({command:seq, token:token}); + }; + } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") { + Environments.push({command:seq, token:token}); + } else if (seq === "input") { + newPos = read1filename(TokeniseResult, i); + if (newPos === null) { continue; } else {i = newPos;}; + } else if (seq === "hbox" || seq === "text" || seq === "mbox") { + nextGroupMathMode = false; + } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) { + var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + if (currentMathMode === null && !insideGroup) { + TokenError(token, type + seq + " must be inside math mode"); + }; + } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) { + currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + if (currentMathMode && !insideGroup) { + TokenError(token, type + seq + " used inside math mode"); + Environments.resetMathMode(); + }; + }; } else if (type === "{") { - Environments.push({command:"{", token:token}); + Environments.push({command:"{", token:token, mathMode: nextGroupMathMode}); + nextGroupMathMode = null; } else if (type === "}") { Environments.push({command:"}", token:token}); - }; + } else if (type === "$") { + var lookAhead = Tokens[i+1]; + var nextIsDollar = lookAhead && lookAhead[1] === "$"; + currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) { + Environments.push({command:"$$", token:token}); + i = i + 1; + } else { + Environments.push({command:"$", token:token}); + } + } else if (type === "^" || type === "_") { + currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) + var insideGroup = Environments.insideGroup(); // true if inside {....} + if (currentMathMode === null && !insideGroup) { + TokenError(token, type + " must be inside math mode"); + }; + } else { + nextGroupMathMode = null; + } }; return Environments; }; - -var CheckEnvironments = function (Environments, ErrorReporter) { +var EnvHandler = function (ErrorReporter) { var ErrorTo = ErrorReporter.EnvErrorTo; var ErrorFromTo = ErrorReporter.EnvErrorFromTo; var ErrorFrom = ErrorReporter.EnvErrorFrom; + var envs = []; + var state = []; var documentClosed = null; var inVerbatim = false; var verbatimRanges = []; - for (var i = 0, len = Environments.length; i < len; i++) { - var name = Environments[i].name ; - if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted)$/)) { - Environments[i].verbatim = true; + + this.Environments = envs; + + this.push = function (newEnv) { + this.setEnvProps(newEnv); + this.checkAndUpdateState(newEnv); + envs.push(newEnv); + }; + + this._endVerbatim = function (thisEnv) { + var lastEnv = state.pop(); + if (lastEnv && lastEnv.name === thisEnv.name) { + inVerbatim = false; + verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]}); + } else { + if(lastEnv) { state.push(lastEnv); } ; } - } - for (i = 0, len = Environments.length; i < len; i++) { - var thisEnv = Environments[i]; - if(thisEnv.command === "begin" || thisEnv.command === "{") { - if (inVerbatim) { continue; } // ignore anything in verbatim environments - if (thisEnv.verbatim) {inVerbatim = true;}; - state.push(thisEnv); - } else if (thisEnv.command === "end" || thisEnv.command === "}") { + }; + + var invalidEnvs = []; + + this._end = function (thisEnv) { + do { var lastEnv = state.pop(); + var retry = false; + var i; - if (inVerbatim) { - if (lastEnv && lastEnv.name === thisEnv.name) { - inVerbatim = false; - verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]}); - continue; - } else { - if(lastEnv) { state.push(lastEnv); } ; - continue; // ignore all other commands - } - }; - - if (lastEnv && lastEnv.command === "{" && thisEnv.command === "}") { - continue; - } else if (lastEnv && lastEnv.name === thisEnv.name) { - if (thisEnv.name === "document" && !documentClosed) { + if (closedBy(lastEnv, thisEnv)) { + if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) { documentClosed = thisEnv; }; - continue; + return; } else if (!lastEnv) { - if (thisEnv.command === "}") { - if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); - } else { - ErrorTo(thisEnv, "unexpected end group }"); - }; - } else if (thisEnv.command === "end") { - if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); - } else { - ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}"); - } + if (documentClosed) { + ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); + } else { + ErrorTo(thisEnv, "unexpected \\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); + } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) { + invalidEnvs.splice(i, 1); + if (lastEnv) { state.push(lastEnv); } ; + return; + } else { + var status = reportError(lastEnv, thisEnv); + if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) { + invalidEnvs.push(lastEnv); + retry = true; + } else { + var prevLastEnv = state.pop(); + if(prevLastEnv) { + if (thisEnv.name === prevLastEnv.name) { + return; + } else { + state.push(prevLastEnv); + } } + invalidEnvs.push(lastEnv); } } + } while (retry === true); + }; + + var CLOSING_DELIMITER = { + "{" : "}", + "left" : "right", + "[" : "]", + "(" : ")", + "$" : "$", + "$$": "$$" + }; + + var closedBy = function (lastEnv, thisEnv) { + if (!lastEnv) { + return false ; + } else if (thisEnv.command === "end") { + return lastEnv.command === "begin" && lastEnv.name === thisEnv.name; + } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) { + return true; + } else { + return false; } - } - while (state.length > 0) { - thisEnv = state.pop(); - if (thisEnv.command === "{") { - ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); - } else if (thisEnv.command === "begin") { - ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}"); + }; + + var indexOfClosingEnvInArray = function (envs, thisEnv) { + for (var i = 0, n = envs.length; i < n ; i++) { + if (closedBy(envs[i], thisEnv)) { + return i; + } + } + return -1; + }; + + var envPrecedence = function (env) { + var openScore = { + "{" : 1, + "left" : 2, + "$" : 3, + "$$" : 4, + "begin": 4 }; - } - var vlen = verbatimRanges.length; - len = ErrorReporter.tokenErrors.length; - if (vlen >0 && len > 0) { - for (i = 0; i < len; i++) { - var tokenError = ErrorReporter.tokenErrors[i]; - var startPos = tokenError.startPos; - var endPos = tokenError.endPos; - for (j = 0; j < vlen; j++) { - if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) { - tokenError.ignore = true; - break; + var closeScore = { + "}" : 1, + "right" : 2, + "$" : 3, + "$$" : 5, + "end": 4 + }; + if (env.command) { + return openScore[env.command] || closeScore[env.command]; + } else { + return 0; + } + }; + + var getName = function(env) { + var description = { + "{" : "open group {", + "}" : "close group }", + "[" : "open display math \\[", + "]" : "close display math \\]", + "(" : "open inline math \\(", + ")" : "close inline math \\)", + "$" : "$", + "$$" : "$$", + "left" : "\\left", + "right" : "\\right" + }; + if (env.command === "begin" || env.command === "end") { + return "\\" + env.command + "{" + env.name + "}"; + } else if (env.command in description) { + return description[env.command]; + } else { + return env.command; + } + }; + + var EXTRA_CLOSE = 1; + var UNCLOSED_GROUP = 2; + var UNCLOSED_ENV = 3; + + var reportError = function(lastEnv, thisEnv) { + if (!lastEnv) { // unexpected close, nothing was open! + if (documentClosed) { + ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); + } else { + ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); + }; + return EXTRA_CLOSE; + } else if (lastEnv.command === "{" && thisEnv.command === "end") { + ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), + {suppressIfEditing:true, errorAtStart: true, type:"warning"}); + return UNCLOSED_GROUP; + } else { + var pLast = envPrecedence(lastEnv); + var pThis = envPrecedence(thisEnv); + if (pThis > pLast) { + ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), + {suppressIfEditing:true, errorAtStart: true}); + } else { + ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv)); + } + return UNCLOSED_ENV; + }; + }; + + this._beginMathMode = function (thisEnv) { + var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env + if (currentMathMode) { + ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode), + {suppressIfEditing:true, errorAtStart: true}); + }; + thisEnv.mathMode = thisEnv; + state.push(thisEnv); + }; + + this._toggleMathMode = function (thisEnv) { + var lastEnv = state.pop(); + if (closedBy(lastEnv, thisEnv)) { + return; + } else { + if (lastEnv) {state.push(lastEnv);} + if (lastEnv && lastEnv.mathMode) { + this._end(thisEnv); + } else { + thisEnv.mathMode = thisEnv; + state.push(thisEnv); + } + }; + }; + + this.getMathMode = function () { + var n = state.length; + if (n > 0) { + return state[n-1].mathMode; + } else { + return null; + } + }; + + this.insideGroup = function () { + var n = state.length; + if (n > 0) { + return (state[n-1].command === "{"); + } else { + return null; + } + }; + + var resetMathMode = function () { + var n = state.length; + if (n > 0) { + var lastMathMode = state[n-1].mathMode; + do { + var lastEnv = state.pop(); + } while (lastEnv && lastEnv !== lastMathMode); + } else { + return; + } + }; + + this.resetMathMode = resetMathMode; + + var getNewMathMode = function (currentMathMode, thisEnv) { + var newMathMode = null; + + if (thisEnv.command === "{") { + if (thisEnv.mathMode !== null) { + newMathMode = thisEnv.mathMode; + } else { + newMathMode = currentMathMode; + } + } else if (thisEnv.command === "left") { + if (currentMathMode === null) { + ErrorFrom(thisEnv, "\\left can only be used in math mode"); + }; + newMathMode = currentMathMode; + } else if (thisEnv.command === "begin") { + var name = thisEnv.name; + if (name) { + if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { + if (currentMathMode) { + ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), + {suppressIfEditing:true, errorAtStart: true}); + resetMathMode(); + }; + newMathMode = null; + } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) { + if (!currentMathMode) { + ErrorFrom(thisEnv, thisEnv.name + " not inside math mode"); + }; + newMathMode = currentMathMode; + } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) { + if (currentMathMode) { + ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), + {suppressIfEditing:true, errorAtStart: true}); + resetMathMode(); + }; + newMathMode = thisEnv; + } else { + newMathMode = undefined; // undefined means we don't know if we are in math mode or not + } + } + }; + return newMathMode; + }; + + this.checkAndUpdateState = function (thisEnv) { + if (inVerbatim) { + if (thisEnv.command === "end") { + this._endVerbatim(thisEnv); + } else { + return; // ignore anything in verbatim environments + } + } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") { + if (thisEnv.verbatim) {inVerbatim = true;}; + var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env + var newMathMode = getNewMathMode(currentMathMode, thisEnv); + thisEnv.mathMode = newMathMode; + state.push(thisEnv); + } else if (thisEnv.command === "end") { + this._end(thisEnv); + } else if (thisEnv.command === "(" || thisEnv.command === "[") { + this._beginMathMode(thisEnv); + } else if (thisEnv.command === ")" || thisEnv.command === "]") { + this._end(thisEnv); + } else if (thisEnv.command === "}") { + this._end(thisEnv); + } else if (thisEnv.command === "right") { + this._end(thisEnv); + } else if (thisEnv.command === "$" || thisEnv.command === "$$") { + this._toggleMathMode(thisEnv); + } + }; + + this.close = function () { + while (state.length > 0) { + var thisEnv = state.pop(); + if (thisEnv.command === "{") { + ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); + } else { + ErrorFrom(thisEnv, "unclosed " + getName(thisEnv)); + } + } + var vlen = verbatimRanges.length; + var len = ErrorReporter.tokenErrors.length; + if (vlen >0 && len > 0) { + for (var i = 0; i < len; i++) { + var tokenError = ErrorReporter.tokenErrors[i]; + var startPos = tokenError.startPos; + var endPos = tokenError.endPos; + for (var j = 0; j < vlen; j++) { + if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) { + tokenError.ignore = true; + break; + } } } } - } + }; + this.setEnvProps = function (env) { + var name = env.name ; + if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) { + env.verbatim = true; + } + }; }; var ErrorReporter = function (TokeniseResult) { var text = TokeniseResult.text; @@ -1931,9 +2229,11 @@ var ErrorReporter = function (TokeniseResult) { return returnedErrors.concat(errors); }; - this.TokenError = function (token, message) { + this.TokenError = function (token, message, options) { + if(!options) { options = { suppressIfEditing:true } ; }; var line = token[0], type = token[1], start = token[2], end = token[3]; var start_col = start - linePosition[line]; + if (!end) { end = start + 1; } ; var end_col = end - linePosition[line]; tokenErrors.push({row: line, column: start_col, @@ -1945,10 +2245,11 @@ var ErrorReporter = function (TokeniseResult) { text:message, startPos: start, endPos: end, - suppressIfEditing:true}); + suppressIfEditing:options.suppressIfEditing}); }; - this.TokenErrorFromTo = function (fromToken, toToken, message) { + this.TokenErrorFromTo = function (fromToken, toToken, message, options) { + if(!options) { options = {suppressIfEditing:true } ; }; var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3]; var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3]; if (!toEnd) { toEnd = toStart + 1;}; @@ -1965,7 +2266,7 @@ var ErrorReporter = function (TokeniseResult) { text:message, startPos: fromStart, endPos: toEnd, - suppressIfEditing:true}); + suppressIfEditing:options.suppressIfEditing}); }; @@ -2027,7 +2328,7 @@ var Parse = function (text) { var TokeniseResult = Tokenise(text); var Reporter = new ErrorReporter(TokeniseResult); var Environments = InterpretTokens(TokeniseResult, Reporter); - CheckEnvironments(Environments, Reporter); + Environments.close(); return Reporter.getErrors(); }; From faf740d224f747433057e4d976356b6357126f6e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 15 Dec 2016 16:23:10 +0000 Subject: [PATCH 13/31] Disable link rewriting. --- services/web/public/coffee/main.coffee | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 471870f280..cf2da7d39e 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -33,4 +33,18 @@ define [ "filters/formatDate" "__MAIN_CLIENTSIDE_INCLUDES__" ], () -> - angular.bootstrap(document.body, ["SharelatexApp"]) + angular.module('SharelatexApp').config( + ($locationProvider) -> + try + $locationProvider.html5Mode({ + enabled: true, + requireBase: false, + rewriteLinks: false + }) + catch e + console.error "Error while trying to fix '#' links: ", e + ) + angular.bootstrap( + document.body, + ["SharelatexApp"] + ) From 47a5eb538202994e09f0c66804a6d42b9f022554 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 15 Dec 2016 17:22:39 +0000 Subject: [PATCH 14/31] Add a new layout and body template for e-mails. --- .../Email/Bodies/SingleCTAEmailBody.coffee | 26 ++ .../coffee/Features/Email/EmailBuilder.coffee | 30 +- .../Layouts/BaseWithHeaderEmailLayout.coffee | 378 ++++++++++++++++++ 3 files changed, 420 insertions(+), 14 deletions(-) create mode 100644 services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee create mode 100644 services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee new file mode 100644 index 0000000000..98a0ffe30d --- /dev/null +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -0,0 +1,26 @@ +_ = require("underscore") +settings = require "settings-sharelatex" + +module.exports = _.template """ + + +
+
+

+ <%= title %> +

+
 
+

+ <%= greeting %> +

+

+ <%= message %> +

+
 
+
+
+ View project +
+
+
+""" diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 70d11e219b..908aac53f1 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -1,6 +1,12 @@ _ = require('underscore') + PersonalEmailLayout = require("./Layouts/PersonalEmailLayout") NotificationEmailLayout = require("./Layouts/NotificationEmailLayout") +BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout") + +SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody") + + settings = require("settings-sharelatex") @@ -107,9 +113,9 @@ If you didn't request a password reset, let us know. templates.projectInvite = subject: _.template "<%= project.name %> - shared by <%= owner.email %>" - layout: NotificationEmailLayout + layout: BaseWithHeaderEmailLayout type:"notification" - plainTextTemplate: _.template """ + plainTextTemplate: plainTextTpl: """ Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Follow this link to view the project: <%= inviteUrl %> @@ -118,18 +124,14 @@ Thank you #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

Hi, <%= owner.email %> wants to share '<%= project.name %>' with you

-
- - - View Project - - -
-

Thank you

-

#{settings.appName}

-""" + compiledTemplate: (opts) -> + SingleCTAEmailBody({ + title: "#{ opts.project.name } – shared by #{ opts.owner.email }" + greeting: "Hi," + message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + ctaURL: opts.inviteUrl + }) + templates.completeJoinGroupAccount = diff --git a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee new file mode 100644 index 0000000000..7b68d3bf49 --- /dev/null +++ b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee @@ -0,0 +1,378 @@ +_ = require("underscore") +settings = require "settings-sharelatex" + +module.exports = _.template """ + + + + + + + + + Project invite + + + + + + + + +
+
+ +
+
+ + +
+
+

+ SHARELATEX +

+
+
+
+
 
+
+
 
+ + <%= body %> + +
+
 
+

ShareLaTeX • www.sharelatex.com

+
+
+ +
+
+ +
                                                           
+ + + +""" From 7dfc2c61a6b3137f95e8d1e7d7668778b7122b7e Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 10:06:56 +0000 Subject: [PATCH 15/31] Make the CTA button text configurable. --- .../coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee | 4 +++- services/web/app/coffee/Features/Email/EmailBuilder.coffee | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee index 98a0ffe30d..07954e2162 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -17,7 +17,9 @@ module.exports = _.template """
 
- View project + + <%= ctaText %> +
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 908aac53f1..dd908c9d42 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -115,7 +115,7 @@ templates.projectInvite = subject: _.template "<%= project.name %> - shared by <%= owner.email %>" layout: BaseWithHeaderEmailLayout type:"notification" - plainTextTemplate: plainTextTpl: """ + plainTextTemplate: """ Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Follow this link to view the project: <%= inviteUrl %> @@ -129,6 +129,7 @@ Thank you title: "#{ opts.project.name } – shared by #{ opts.owner.email }" greeting: "Hi," message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + ctaText: "View project" ctaURL: opts.inviteUrl }) From fdcf97f877ec39e1a60943980677741a687600de Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 16 Dec 2016 10:26:25 +0000 Subject: [PATCH 16/31] Disable angular html5Mode. --- services/web/public/coffee/main.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index cf2da7d39e..5ad6f37d34 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -37,7 +37,7 @@ define [ ($locationProvider) -> try $locationProvider.html5Mode({ - enabled: true, + enabled: false, requireBase: false, rewriteLinks: false }) From 2234c438a165d299baa754f456e8036958fc28dc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 12:16:33 +0000 Subject: [PATCH 17/31] Support a secondary message in the single CTA email template. --- .../coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee index 07954e2162..6d0756596c 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -22,6 +22,12 @@ module.exports = _.template """ + <% if (secondaryMessage) { %> +
 
+

+ <%= secondaryMessage %> +

+ <% } %> From 0fe3664a820a173de07c05a30a130570475ed4be Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 12:16:45 +0000 Subject: [PATCH 18/31] Use the new template in the reset pwd email. --- .../coffee/Features/Email/EmailBuilder.coffee | 35 ++++++------------- .../Layouts/BaseWithHeaderEmailLayout.coffee | 4 ++- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index dd908c9d42..97a1023e3b 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -84,31 +84,15 @@ Thank you #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

Password Reset

-

-We got a request to reset your #{settings.appName} password. -

-

- -
- -If you ignore this message, your password won't be changed. -

-If you didn't request a password reset, let us know. - -

-

Thank you

-

#{settings.appName}

-""" + compiledTemplate: (opts) -> + SingleCTAEmailBody({ + title: "Password Reset" + greeting: "Hi," + message: "We got a request to reset your #{settings.appName} password." + secondaryMessage: "If you ignore this message, your password won't be changed.
If you didn't request a password reset, let us know." + ctaText: "Reset password" + ctaURL: opts.setNewPasswordUrl + }) templates.projectInvite = @@ -129,6 +113,7 @@ Thank you title: "#{ opts.project.name } – shared by #{ opts.owner.email }" greeting: "Hi," message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl }) diff --git a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee index 7b68d3bf49..6d25df2197 100644 --- a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee +++ b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee @@ -362,7 +362,9 @@ module.exports = _.template """
 
-

ShareLaTeX • www.sharelatex.com

+

+ #{ settings.appName} • #{ settings.siteUrl } +

From 3a8a12fcb32d8e0f96bc80bbe7d67caf64710fbe Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 16 Dec 2016 14:07:47 +0000 Subject: [PATCH 19/31] Revert "rollout linter math mode to all users" This reverts commit 0ac0a11d3028cabac53d1e347b2509cb9e785f9f. --- .../web/public/js/ace-1.2.5/mode-latex.js | 14 +- .../web/public/js/ace-1.2.5/worker-latex.js | 507 ++++-------------- 2 files changed, 107 insertions(+), 414 deletions(-) diff --git a/services/web/public/js/ace-1.2.5/mode-latex.js b/services/web/public/js/ace-1.2.5/mode-latex.js index 8e7bbe4802..f183d7c263 100644 --- a/services/web/public/js/ace-1.2.5/mode-latex.js +++ b/services/web/public/js/ace-1.2.5/mode-latex.js @@ -242,10 +242,6 @@ var createLatexWorker = function (session) { var annotations = []; var newRange = {}; var cursor = selection.getCursor(); - var maxRow = session.getLength() - 1; - var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0; - var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol); - suppressions = []; for (var i = 0, len = hints.length; i 0) { - return j; // advance past these tokens - } else { - return null; - } -}; - var readOptionalParams = function(TokeniseResult, k) { var Tokens = TokeniseResult.tokens; var text = TokeniseResult.text; @@ -1715,6 +1697,7 @@ var readUrl = function(TokeniseResult, k) { return null; }; + var InterpretTokens = function (TokeniseResult, ErrorReporter) { var Tokens = TokeniseResult.tokens; var linePosition = TokeniseResult.linePosition; @@ -1723,9 +1706,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo; var TokenError = ErrorReporter.TokenError; - var Environments = new EnvHandler(ErrorReporter); - - var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox) + var Environments = []; for (var i = 0, len = Tokens.length; i < len; i++) { var token = Tokens[i]; @@ -1810,407 +1791,128 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { } else if (seq === "url") { newPos = readUrl(TokeniseResult, i); if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;}; - } else if (seq === "left" || seq === "right") { - var nextToken = Tokens[i+1]; - char = ""; - if (nextToken && nextToken[1] === "Text") { - char = text.substring(nextToken[2], nextToken[2] + 1); - } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") { - char = nextToken[4]; - } else if (nextToken && nextToken[1] === "\\") { - char = "unknown"; - } - if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) { - TokenError(token, "invalid bracket command"); - } else { - i = i + 1; - Environments.push({command:seq, token:token}); - }; - } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") { - Environments.push({command:seq, token:token}); - } else if (seq === "input") { - newPos = read1filename(TokeniseResult, i); - if (newPos === null) { continue; } else {i = newPos;}; - } else if (seq === "hbox" || seq === "text" || seq === "mbox") { - nextGroupMathMode = false; - } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) { - var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (currentMathMode === null && !insideGroup) { - TokenError(token, type + seq + " must be inside math mode"); - }; - } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) { - currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (currentMathMode && !insideGroup) { - TokenError(token, type + seq + " used inside math mode"); - Environments.resetMathMode(); - }; - }; + } } else if (type === "{") { - Environments.push({command:"{", token:token, mathMode: nextGroupMathMode}); - nextGroupMathMode = null; + Environments.push({command:"{", token:token}); } else if (type === "}") { Environments.push({command:"}", token:token}); - } else if (type === "$") { - var lookAhead = Tokens[i+1]; - var nextIsDollar = lookAhead && lookAhead[1] === "$"; - currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) { - Environments.push({command:"$$", token:token}); - i = i + 1; - } else { - Environments.push({command:"$", token:token}); - } - } else if (type === "^" || type === "_") { - currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - var insideGroup = Environments.insideGroup(); // true if inside {....} - if (currentMathMode === null && !insideGroup) { - TokenError(token, type + " must be inside math mode"); - }; - } else { - nextGroupMathMode = null; - } + }; }; return Environments; }; -var EnvHandler = function (ErrorReporter) { + +var CheckEnvironments = function (Environments, ErrorReporter) { var ErrorTo = ErrorReporter.EnvErrorTo; var ErrorFromTo = ErrorReporter.EnvErrorFromTo; var ErrorFrom = ErrorReporter.EnvErrorFrom; - var envs = []; - var state = []; var documentClosed = null; var inVerbatim = false; var verbatimRanges = []; - - this.Environments = envs; - - this.push = function (newEnv) { - this.setEnvProps(newEnv); - this.checkAndUpdateState(newEnv); - envs.push(newEnv); - }; - - this._endVerbatim = function (thisEnv) { - var lastEnv = state.pop(); - if (lastEnv && lastEnv.name === thisEnv.name) { - inVerbatim = false; - verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]}); - } else { - if(lastEnv) { state.push(lastEnv); } ; + for (var i = 0, len = Environments.length; i < len; i++) { + var name = Environments[i].name ; + if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted)$/)) { + Environments[i].verbatim = true; } - }; - - var invalidEnvs = []; - - this._end = function (thisEnv) { - do { + } + 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(); - var retry = false; - var i; - if (closedBy(lastEnv, thisEnv)) { - if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) { + 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; }; - return; + continue; } else if (!lastEnv) { - 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 (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) { - invalidEnvs.splice(i, 1); - if (lastEnv) { state.push(lastEnv); } ; - return; - } else { - var status = reportError(lastEnv, thisEnv); - if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) { - invalidEnvs.push(lastEnv); - retry = true; - } else { - var prevLastEnv = state.pop(); - if(prevLastEnv) { - if (thisEnv.name === prevLastEnv.name) { - return; - } else { - state.push(prevLastEnv); - } + 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); } - invalidEnvs.push(lastEnv); } } - } while (retry === true); - }; - - var CLOSING_DELIMITER = { - "{" : "}", - "left" : "right", - "[" : "]", - "(" : ")", - "$" : "$", - "$$": "$$" - }; - - var closedBy = function (lastEnv, thisEnv) { - if (!lastEnv) { - return false ; - } else if (thisEnv.command === "end") { - return lastEnv.command === "begin" && lastEnv.name === thisEnv.name; - } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) { - return true; - } else { - return false; } - }; - - var indexOfClosingEnvInArray = function (envs, thisEnv) { - for (var i = 0, n = envs.length; i < n ; i++) { - if (closedBy(envs[i], thisEnv)) { - return i; - } - } - return -1; - }; - - var envPrecedence = function (env) { - var openScore = { - "{" : 1, - "left" : 2, - "$" : 3, - "$$" : 4, - "begin": 4 - }; - var closeScore = { - "}" : 1, - "right" : 2, - "$" : 3, - "$$" : 5, - "end": 4 - }; - if (env.command) { - return openScore[env.command] || closeScore[env.command]; - } else { - return 0; - } - }; - - var getName = function(env) { - var description = { - "{" : "open group {", - "}" : "close group }", - "[" : "open display math \\[", - "]" : "close display math \\]", - "(" : "open inline math \\(", - ")" : "close inline math \\)", - "$" : "$", - "$$" : "$$", - "left" : "\\left", - "right" : "\\right" - }; - if (env.command === "begin" || env.command === "end") { - return "\\" + env.command + "{" + env.name + "}"; - } else if (env.command in description) { - return description[env.command]; - } else { - return env.command; - } - }; - - var EXTRA_CLOSE = 1; - var UNCLOSED_GROUP = 2; - var UNCLOSED_ENV = 3; - - var reportError = function(lastEnv, thisEnv) { - if (!lastEnv) { // unexpected close, nothing was open! - if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); - } else { - ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); - }; - return EXTRA_CLOSE; - } else if (lastEnv.command === "{" && thisEnv.command === "end") { - ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), - {suppressIfEditing:true, errorAtStart: true, type:"warning"}); - return UNCLOSED_GROUP; - } else { - var pLast = envPrecedence(lastEnv); - var pThis = envPrecedence(thisEnv); - if (pThis > pLast) { - ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), - {suppressIfEditing:true, errorAtStart: true}); - } else { - ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv)); - } - return UNCLOSED_ENV; - }; - }; - - this._beginMathMode = function (thisEnv) { - var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env - if (currentMathMode) { - ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true}); - }; - thisEnv.mathMode = thisEnv; - state.push(thisEnv); - }; - - this._toggleMathMode = function (thisEnv) { - var lastEnv = state.pop(); - if (closedBy(lastEnv, thisEnv)) { - return; - } else { - if (lastEnv) {state.push(lastEnv);} - if (lastEnv && lastEnv.mathMode) { - this._end(thisEnv); - } else { - thisEnv.mathMode = thisEnv; - state.push(thisEnv); - } - }; - }; - - this.getMathMode = function () { - var n = state.length; - if (n > 0) { - return state[n-1].mathMode; - } else { - return null; - } - }; - - this.insideGroup = function () { - var n = state.length; - if (n > 0) { - return (state[n-1].command === "{"); - } else { - return null; - } - }; - - var resetMathMode = function () { - var n = state.length; - if (n > 0) { - var lastMathMode = state[n-1].mathMode; - do { - var lastEnv = state.pop(); - } while (lastEnv && lastEnv !== lastMathMode); - } else { - return; - } - }; - - this.resetMathMode = resetMathMode; - - var getNewMathMode = function (currentMathMode, thisEnv) { - var newMathMode = null; - + } + while (state.length > 0) { + thisEnv = state.pop(); if (thisEnv.command === "{") { - if (thisEnv.mathMode !== null) { - newMathMode = thisEnv.mathMode; - } else { - newMathMode = currentMathMode; - } - } else if (thisEnv.command === "left") { - if (currentMathMode === null) { - ErrorFrom(thisEnv, "\\left can only be used in math mode"); - }; - newMathMode = currentMathMode; + ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); } else if (thisEnv.command === "begin") { - var name = thisEnv.name; - if (name) { - if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { - if (currentMathMode) { - ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true}); - resetMathMode(); - }; - newMathMode = null; - } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) { - if (!currentMathMode) { - ErrorFrom(thisEnv, thisEnv.name + " not inside math mode"); - }; - newMathMode = currentMathMode; - } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) { - if (currentMathMode) { - ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true}); - resetMathMode(); - }; - newMathMode = thisEnv; - } else { - newMathMode = undefined; // undefined means we don't know if we are in math mode or not - } - } + ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}"); }; - return newMathMode; - }; - - this.checkAndUpdateState = function (thisEnv) { - if (inVerbatim) { - if (thisEnv.command === "end") { - this._endVerbatim(thisEnv); - } else { - return; // ignore anything in verbatim environments - } - } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") { - if (thisEnv.verbatim) {inVerbatim = true;}; - var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env - var newMathMode = getNewMathMode(currentMathMode, thisEnv); - thisEnv.mathMode = newMathMode; - state.push(thisEnv); - } else if (thisEnv.command === "end") { - this._end(thisEnv); - } else if (thisEnv.command === "(" || thisEnv.command === "[") { - this._beginMathMode(thisEnv); - } else if (thisEnv.command === ")" || thisEnv.command === "]") { - this._end(thisEnv); - } else if (thisEnv.command === "}") { - this._end(thisEnv); - } else if (thisEnv.command === "right") { - this._end(thisEnv); - } else if (thisEnv.command === "$" || thisEnv.command === "$$") { - this._toggleMathMode(thisEnv); - } - }; - - this.close = function () { - while (state.length > 0) { - var thisEnv = state.pop(); - if (thisEnv.command === "{") { - ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); - } else { - ErrorFrom(thisEnv, "unclosed " + getName(thisEnv)); - } - } - var vlen = verbatimRanges.length; - var len = ErrorReporter.tokenErrors.length; - if (vlen >0 && len > 0) { - for (var i = 0; i < len; i++) { - var tokenError = ErrorReporter.tokenErrors[i]; - var startPos = tokenError.startPos; - var endPos = tokenError.endPos; - for (var j = 0; j < vlen; j++) { - if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) { - tokenError.ignore = true; - break; - } + } + 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; } } } - }; + } - this.setEnvProps = function (env) { - var name = env.name ; - if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) { - env.verbatim = true; - } - }; }; var ErrorReporter = function (TokeniseResult) { var text = TokeniseResult.text; @@ -2229,11 +1931,9 @@ var ErrorReporter = function (TokeniseResult) { return returnedErrors.concat(errors); }; - this.TokenError = function (token, message, options) { - if(!options) { options = { suppressIfEditing:true } ; }; + this.TokenError = function (token, message) { var line = token[0], type = token[1], start = token[2], end = token[3]; var start_col = start - linePosition[line]; - if (!end) { end = start + 1; } ; var end_col = end - linePosition[line]; tokenErrors.push({row: line, column: start_col, @@ -2245,11 +1945,10 @@ var ErrorReporter = function (TokeniseResult) { text:message, startPos: start, endPos: end, - suppressIfEditing:options.suppressIfEditing}); + suppressIfEditing:true}); }; - this.TokenErrorFromTo = function (fromToken, toToken, message, options) { - if(!options) { options = {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;}; @@ -2266,7 +1965,7 @@ var ErrorReporter = function (TokeniseResult) { text:message, startPos: fromStart, endPos: toEnd, - suppressIfEditing:options.suppressIfEditing}); + suppressIfEditing:true}); }; @@ -2328,7 +2027,7 @@ var Parse = function (text) { var TokeniseResult = Tokenise(text); var Reporter = new ErrorReporter(TokeniseResult); var Environments = InterpretTokens(TokeniseResult, Reporter); - Environments.close(); + CheckEnvironments(Environments, Reporter); return Reporter.getErrors(); }; From ca5040882f5134c436a4460997ab2629cbc2f21d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 14:57:59 +0000 Subject: [PATCH 20/31] Use the new template in the join group email. --- .../coffee/Features/Email/EmailBuilder.coffee | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 97a1023e3b..c083334bed 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -67,7 +67,7 @@ ShareLaTeX Co-founder templates.passwordResetRequested = subject: _.template "Password Reset - #{settings.appName}" - layout: NotificationEmailLayout + layout: BaseWithHeaderEmailLayout type:"notification" plainTextTemplate: _.template """ Password Reset @@ -122,7 +122,7 @@ Thank you templates.completeJoinGroupAccount = subject: _.template "Verify Email to join <%= group_name %> group" - layout: NotificationEmailLayout + layout: BaseWithHeaderEmailLayout type:"notification" plainTextTemplate: _.template """ Hi, please verify your email to join the <%= group_name %> and get your free premium account @@ -133,23 +133,15 @@ Thank You #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

Hi, please verify your email to join the <%= group_name %> and get your free premium account

-
- -
-

Thank you

-

#{settings.appName}

-""" - + compiledTemplate: (opts) -> + SingleCTAEmailBody({ + title: "Verify Email to join #{ opts.group_name } group" + greeting: "Hi," + message: "please verify your email to join the #{ opts.group_name } group and get your free premium account." + secondaryMessage: null + ctaText: "Verify now" + ctaURL: opts.completeJoinUrl + }) module.exports = templates: templates @@ -165,4 +157,4 @@ module.exports = html: template.layout(opts) text: template?.plainTextTemplate?(opts) type:template.type - } + } \ No newline at end of file From 869f729132ff170fb2b5c1d3d904cb307ba1aba8 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 15:15:06 +0000 Subject: [PATCH 21/31] Fix forgotten templating function. --- services/web/app/coffee/Features/Email/EmailBuilder.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index c083334bed..0a5f653178 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -99,7 +99,7 @@ templates.projectInvite = subject: _.template "<%= project.name %> - shared by <%= owner.email %>" layout: BaseWithHeaderEmailLayout type:"notification" - plainTextTemplate: """ + plainTextTemplate: _.template """ Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Follow this link to view the project: <%= inviteUrl %> From d6fcc21ab571267d61827e1f0f6f0c6f0c8e67cb Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 17:04:26 +0000 Subject: [PATCH 22/31] Add support for GMail go-to actions in the single CTA template. --- .../Email/Bodies/SingleCTAEmailBody.coffee | 15 +++++++++++++++ .../app/coffee/Features/Email/EmailBuilder.coffee | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee index 6d0756596c..00a878c276 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -31,4 +31,19 @@ module.exports = _.template """ +<% if (gmailGoToAction) { %> + +<% } %> """ diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 0a5f653178..306aad3d2a 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -92,6 +92,7 @@ Thank you secondaryMessage: "If you ignore this message, your password won't be changed.
If you didn't request a password reset, let us know." ctaText: "Reset password" ctaURL: opts.setNewPasswordUrl + gmailGoToAction: null }) @@ -116,6 +117,10 @@ Thank you secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl + gmailGoToAction: + target: opts.inviteUrl + name: "View project" + description: "Join #{ opts.project.name } at ShareLaTeX" }) @@ -141,6 +146,7 @@ Thank You secondaryMessage: null ctaText: "Verify now" ctaURL: opts.completeJoinUrl + gmailGoToAction: null }) module.exports = From 259c589076a6b2dc736f1f118071e3313b45ff34 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 20 Dec 2016 09:54:42 +0000 Subject: [PATCH 23/31] Add option to restrict invites to existing user accounts. --- .../CollaboratorsInviteController.coffee | 28 +++- .../web/app/views/project/editor/share.jade | 9 +- services/web/config/settings.defaults.coffee | 4 + .../ShareProjectModalController.coffee | 28 ++-- .../public/stylesheets/app/editor/share.less | 8 +- .../CollaboratorsInviteControllerTests.coffee | 158 ++++++++++++++++++ 6 files changed, 217 insertions(+), 18 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index b88ded33b2..9d9f4d2a5e 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -4,6 +4,7 @@ UserGetter = require "../User/UserGetter" CollaboratorsHandler = require('./CollaboratorsHandler') CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') logger = require('logger-sharelatex') +Settings = require('settings-sharelatex') EmailHelper = require "../Helpers/EmailHelper" EditorRealTimeController = require("../Editor/EditorRealTimeController") NotificationsBuilder = require("../Notifications/NotificationsBuilder") @@ -21,6 +22,16 @@ module.exports = CollaboratorsInviteController = return next(err) res.json({invites: invites}) + _checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) -> + if Settings.restrictInvitesToExistingAccounts == true + logger.log {email}, "checking if user exists with this email" + UserGetter.getUser {email: email}, {_id: 1}, (err, user) -> + return callback(err) if err? + userExists = user? and user?._id? + callback(null, userExists) + else + callback(null, true) + inviteToProject: (req, res, next) -> projectId = req.params.Project_id email = req.body.email @@ -37,13 +48,20 @@ module.exports = CollaboratorsInviteController = if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) - CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> + CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> if err? - logger.err {projectId, email, sendingUserId}, "error creating project invite" + logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" return next(err) - logger.log {projectId, email, sendingUserId}, "invite created" - EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) - return res.json {invite: invite} + if !shouldAllowInvite + logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address" + return res.json {invite: null, error: 'cannot_invite_non_user'} + CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> + if err? + logger.err {projectId, email, sendingUserId}, "error creating project invite" + return next(err) + logger.log {projectId, email, sendingUserId}, "invite created" + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) + return res.json {invite: invite} revokeInvite: (req, res, next) -> projectId = req.params.Project_id diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index fd13ccb240..62de414064 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -137,10 +137,15 @@ script(type='text/ng-template', id='shareProjectModalTemplate') p.small(ng-show="startedFreeTrial") | #{translate("refresh_page_after_starting_free_trial")}. - .modal-footer + .modal-footer.modal-footer-share .modal-footer-left i.fa.fa-refresh.fa-spin(ng-show="state.inflight") - span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} + span.text-danger.error(ng-show="state.error") + span(ng-switch="state.errorReason") + span(ng-switch-when="cannot_invite_non_user") + | #{translate("cannot_invite_non_user")} + span(ng-switch-default) + | #{translate("generic_something_went_wrong")} button.btn.btn-default( ng-click="done()" ) #{translate("close")} diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index e3b842a0d2..ccfec59235 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -276,6 +276,10 @@ module.exports = settings = # Cookie max age (in milliseconds). Set to false for a browser session. cookieSessionLength: 5 * 24 * 60 * 60 * 1000 # 5 days + # When true, only allow invites to be sent to email addresses that + # already have user accounts + restrictInvitesToExistingAccounts: false + # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 2d27f96970..6ab15de766 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -8,6 +8,7 @@ define [ } $scope.state = { error: null + errorReason: null inflight: false startedFreeTrial: false invites: [] @@ -69,7 +70,8 @@ define [ members = $scope.inputs.contacts $scope.inputs.contacts = [] - $scope.state.error = null + $scope.state.error = false + $scope.state.errorReason = null $scope.state.inflight = true if !$scope.project.invites? @@ -101,17 +103,22 @@ define [ request .success (data) -> - if data.invite - invite = data.invite - $scope.project.invites.push invite + if data.error + $scope.state.error = true + $scope.state.errorReason = "#{data.error}" + $scope.state.inflight = false else - if data.users? - users = data.users - else if data.user? - users = [data.user] + if data.invite + invite = data.invite + $scope.project.invites.push invite else - users = [] - $scope.project.members.push users... + if data.users? + users = data.users + else if data.user? + users = [data.user] + else + users = [] + $scope.project.members.push users... setTimeout () -> # Give $scope a chance to update $scope.canAddCollaborators @@ -121,6 +128,7 @@ define [ .error () -> $scope.state.inflight = false $scope.state.error = true + $scope.state.errorReason = null $timeout addMembers, 50 # Give email list a chance to update diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less index cd06a15313..9efa8fdbad 100644 --- a/services/web/public/stylesheets/app/editor/share.less +++ b/services/web/public/stylesheets/app/editor/share.less @@ -47,4 +47,10 @@ } } } -} \ No newline at end of file +} +.modal-footer-share { + .modal-footer-left { + max-width: 70%; + text-align: left; + } +} diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index f01e2c7015..28bf1ab6a2 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -27,6 +27,7 @@ describe "CollaboratorsInviteController", -> "../Notifications/NotificationsBuilder": @NotificationsBuilder = {} "../Analytics/AnalyticsManager": @AnalyticsManger '../Authentication/AuthenticationController': @AuthenticationController + 'settings-sharelatex': @settings = {} @res = new MockResponse() @req = new MockRequest() @@ -103,9 +104,15 @@ describe "CollaboratorsInviteController", -> describe 'when all goes well', -> beforeEach -> + @_checkShouldInviteEmail = sinon.stub( + @CollaboratorsInviteController, '_checkShouldInviteEmail' + ).callsArgWith(1, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next + afterEach -> + @_checkShouldInviteEmail.restore() + it 'should produce json response', -> @res.json.callCount.should.equal 1 ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0]) @@ -114,6 +121,10 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true + it 'should have called _checkShouldInviteEmail', -> + @_checkShouldInviteEmail.callCount.should.equal 1 + @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true @@ -125,37 +136,63 @@ describe "CollaboratorsInviteController", -> describe 'when the user is not allowed to add more collaborators', -> beforeEach -> + @_checkShouldInviteEmail = sinon.stub( + @CollaboratorsInviteController, '_checkShouldInviteEmail' + ).callsArgWith(1, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @CollaboratorsInviteController.inviteToProject @req, @res, @next + afterEach -> + @_checkShouldInviteEmail.restore() + it 'should produce json response without an invite', -> @res.json.callCount.should.equal 1 ({invite: null}).should.deep.equal(@res.json.firstCall.args[0]) + it 'should not have called _checkShouldInviteEmail', -> + @_checkShouldInviteEmail.callCount.should.equal 0 + @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false + it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 describe 'when canAddXCollaborators produces an error', -> beforeEach -> + @_checkShouldInviteEmail = sinon.stub( + @CollaboratorsInviteController, '_checkShouldInviteEmail' + ).callsArgWith(1, null, true) @err = new Error('woops') @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err) @CollaboratorsInviteController.inviteToProject @req, @res, @next + afterEach -> + @_checkShouldInviteEmail.restore() + it 'should call next with an error', -> @next.callCount.should.equal 1 @next.calledWith(@err).should.equal true + it 'should not have called _checkShouldInviteEmail', -> + @_checkShouldInviteEmail.callCount.should.equal 0 + @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false + it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 describe 'when inviteToProject produces an error', -> beforeEach -> + @_checkShouldInviteEmail = sinon.stub( + @CollaboratorsInviteController, '_checkShouldInviteEmail' + ).callsArgWith(1, null, true) @err = new Error('woops') @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err) @CollaboratorsInviteController.inviteToProject @req, @res, @next + afterEach -> + @_checkShouldInviteEmail.restore() + it 'should call next with an error', -> @next.callCount.should.equal 1 @next.calledWith(@err).should.equal true @@ -164,10 +201,60 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.callCount.should.equal 1 @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true + it 'should have called _checkShouldInviteEmail', -> + @_checkShouldInviteEmail.callCount.should.equal 1 + @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true + describe 'when _checkShouldInviteEmail disallows the invite', -> + + beforeEach -> + @_checkShouldInviteEmail = sinon.stub( + @CollaboratorsInviteController, '_checkShouldInviteEmail' + ).callsArgWith(1, null, false) + @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) + @CollaboratorsInviteController.inviteToProject @req, @res, @next + + afterEach -> + @_checkShouldInviteEmail.restore() + + it 'should produce json response with no invite, and an error property', -> + @res.json.callCount.should.equal 1 + ({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0]) + + it 'should have called _checkShouldInviteEmail', -> + @_checkShouldInviteEmail.callCount.should.equal 1 + @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + + it 'should not have called inviteToProject', -> + @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 + + describe 'when _checkShouldInviteEmail produces an error', -> + + beforeEach -> + @_checkShouldInviteEmail = sinon.stub( + @CollaboratorsInviteController, '_checkShouldInviteEmail' + ).callsArgWith(1, new Error('woops')) + @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) + @CollaboratorsInviteController.inviteToProject @req, @res, @next + + afterEach -> + @_checkShouldInviteEmail.restore() + + it 'should call next with an error', -> + @next.callCount.should.equal 1 + @next.calledWith(@err).should.equal true + + it 'should have called _checkShouldInviteEmail', -> + @_checkShouldInviteEmail.callCount.should.equal 1 + @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + + it 'should not have called inviteToProject', -> + @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 + describe "viewInvite", -> beforeEach -> @@ -579,3 +666,74 @@ describe "CollaboratorsInviteController", -> it 'should have called acceptInvite', -> @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 + + describe '_checkShouldInviteEmail', -> + + beforeEach -> + @email = 'user@example.com' + @call = (callback) => + @CollaboratorsInviteController._checkShouldInviteEmail @email, callback + + describe 'when we should be restricting to existing accounts', -> + + beforeEach -> + @settings.restrictInvitesToExistingAccounts = true + + describe 'when user account is present', -> + + beforeEach -> + @user = {_id: ObjectId().toString()} + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) + + it 'should callback with `true`', (done) -> + @call (err, shouldAllow) => + expect(err).to.equal null + expect(shouldAllow).to.equal true + done() + + describe 'when user account is absent', -> + + beforeEach -> + @user = null + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) + + it 'should callback with `false`', (done) -> + @call (err, shouldAllow) => + expect(err).to.equal null + expect(shouldAllow).to.equal false + done() + + it 'should have called getUser', (done) -> + @call (err, shouldAllow) => + @UserGetter.getUser.callCount.should.equal 1 + @UserGetter.getUser.calledWith({email: @email}, {_id: 1}).should.equal true + done() + + describe 'when getUser produces an error', -> + + beforeEach -> + @user = null + @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops')) + + it 'should callback with an error', (done) -> + @call (err, shouldAllow) => + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + expect(shouldAllow).to.equal undefined + done() + + describe 'when we should not be restricting', -> + + beforeEach -> + @settings.restrictInvitesToExistingAccounts = false + + it 'should callback with `true`', (done) -> + @call (err, shouldAllow) => + expect(err).to.equal null + expect(shouldAllow).to.equal true + done() + + it 'should not have called getUser', (done) -> + @call (err, shouldAllow) => + @UserGetter.getUser.callCount.should.equal 0 + done() From 7bbbfe20b9c6cfd6ba8e74926c43a61d783fb240 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 21 Dec 2016 13:50:13 +0000 Subject: [PATCH 24/31] If external auth is used, remove `/register` items from header nav. (logic moved from docker-image settings file) --- .../app/coffee/infrastructure/ExpressLocals.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index e469df9422..d976a1d23a 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -244,6 +244,17 @@ module.exports = (app, webRouter, apiRouter)-> for key, value of Settings.nav res.locals.nav[key] = _.clone(Settings.nav[key]) res.locals.templates = Settings.templateLinks + try + externalAuth = res.locals.externalAuthenticationSystemUsed() + if externalAuth and res.locals.nav.header? + # filter out '/register' link + res.locals.nav.header = _.filter( + res.locals.nav.header, + (h) -> + h.url != '/register' + ) + catch error + logger.error {error}, "error while trying to filter out '/register' links from header" next() webRouter.use (req, res, next) -> From 862e15b8429a3a78fc9d5bc7b1bdf6a35484a985 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 5 Jan 2017 15:02:10 +0000 Subject: [PATCH 25/31] log out user id and anonymous when loading editor --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 1d975ea5b3..44933fdcbc 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -197,7 +197,7 @@ module.exports = ProjectController = user_id = null project_id = req.params.Project_id - logger.log project_id:project_id, "loading editor" + logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor" async.parallel { project: (cb)-> From 84ce2d0e14b720f4a2c74894295c3e8ad1bdf0ed Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 6 Jan 2017 11:00:21 +0000 Subject: [PATCH 26/31] change default nav to use translations for login and register --- services/web/config/settings.defaults.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index ccfec59235..708892e2fa 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -336,11 +336,11 @@ module.exports = settings = }] header: [{ - text: "Register" + text: "register" url: "/register" only_when_logged_out: true }, { - text: "Log In" + text: "log_in" url: "/login" only_when_logged_out: true }, { From 0b67265eb61b1f57da4450c9bbef9dd2eb5c7a8e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 6 Jan 2017 11:32:57 +0000 Subject: [PATCH 27/31] use admin email for closed site --- services/web/app/views/general/closed.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/general/closed.jade b/services/web/app/views/general/closed.jade index 9f21372c81..4a27e84681 100644 --- a/services/web/app/views/general/closed.jade +++ b/services/web/app/views/general/closed.jade @@ -11,4 +11,4 @@ block content | Sorry, ShareLaTeX is briefly down for maintenance. | We should be back within minutes, but if not, or you have | an urgent request, please contact us at - | support@sharelatex.com + | #{settings.adminEmail} From f5ced0307479d6f22d9bb98be3b02ba9039dc46b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 10 Jan 2017 15:42:36 +0000 Subject: [PATCH 28/31] Set redirect when sending user to `login` page. Allows smart redirecting to work when public access is turned off. --- .../Authentication/AuthenticationController.coffee | 1 + .../Authentication/AuthenticationControllerTests.coffee | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e406296730..e8e3db4f93 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -148,6 +148,7 @@ module.exports = AuthenticationController = return next() else logger.log url:req.url, "user trying to access endpoint not in global whitelist" + AuthenticationController._setRedirectInSession(req) return res.redirect "/login" httpAuth: basicAuth (user, pass)-> diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 72265eac11..515b888911 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -387,6 +387,10 @@ describe "AuthenticationController", -> beforeEach -> @req.headers = {} @AuthenticationController.httpAuth = sinon.stub() + @_setRedirect = sinon.spy(@AuthenticationController, '_setRedirectInSession') + + afterEach -> + @_setRedirect.restore() describe "with white listed url", -> beforeEach -> @@ -431,6 +435,9 @@ describe "AuthenticationController", -> @req.session = {} @AuthenticationController.requireGlobalLogin @req, @res, @next + it 'should have called setRedirectInSession', -> + @_setRedirect.callCount.should.equal 1 + it "should redirect to the /login page", -> @res.redirectedTo.should.equal "/login" From 731f280e2e6902eb2462b5b3c83b0fc848e4c1df Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 11 Jan 2017 10:27:38 +0000 Subject: [PATCH 29/31] Move auth parts of top menu out of config and into web templates. Move the remaining configuration into a new config var: `nav.header_extras`. Add a `nav.showSubscriptionLink` var to control visibility of subscription link in the Account menu. This will allow admins to more easily configure extra links in the top navigation bar, without the danger of overwriting the important auth menus. --- .../infrastructure/ExpressLocals.coffee | 13 +----- services/web/app/views/layout/navbar.jade | 42 +++++++++++++++---- services/web/config/settings.defaults.coffee | 34 +++------------ 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index d976a1d23a..867583468b 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -244,17 +244,8 @@ module.exports = (app, webRouter, apiRouter)-> for key, value of Settings.nav res.locals.nav[key] = _.clone(Settings.nav[key]) res.locals.templates = Settings.templateLinks - try - externalAuth = res.locals.externalAuthenticationSystemUsed() - if externalAuth and res.locals.nav.header? - # filter out '/register' link - res.locals.nav.header = _.filter( - res.locals.nav.header, - (h) -> - h.url != '/register' - ) - catch error - logger.error {error}, "error while trying to filter out '/register' links from header" + if res.locals.nav.header + console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead" next() webRouter.use (req, res, next) -> diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 3cd6587592..8ec5b5dbf3 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -24,7 +24,10 @@ nav.navbar.navbar-default li a(href="/admin/user") Manage Users - each item in nav.header + + // loop over header_extras + each item in nav.header_extras + if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in)) if item.dropdown li.dropdown(class=item.class, dropdown) @@ -35,9 +38,6 @@ nav.navbar.navbar-default each child in item.dropdown if child.divider li.divider - else if child.user_email - li - div.subdued #{getUserEmail()} else li if child.url @@ -50,7 +50,35 @@ nav.navbar.navbar-default a(href=item.url, class=item.class) !{translate(item.text)} else | !{translate(item.text)} - - - + // logged out + if !getSessionUser() + // register link + if !externalAuthenticationSystemUsed() + li + a(href="/register") #{translate('register')} + + // login link + li + a(href="/login") #{translate('log_in')} + + // projects link and account menu + if getSessionUser() + li + a(href="/projects") #{translate('Projects')} + li.dropdown(dropdown) + a.dropbodw-toggle(href, dropdown-toggle) + | #{translate('Account')} + b.caret + ul.dropdown-menu + li + div.subdued #{getUserEmail()} + li.divider + li + a(href="/user/settings") #{translate('Account Settings')} + if nav.showSubscriptionLink + li + a(href="/user/subscription") #{translate('subscription')} + li.divider + li + a(href="/logout") #{translate('log_out')} diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 708892e2fa..44e4a75867 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -335,35 +335,11 @@ module.exports = settings = url: "https://github.com/sharelatex/sharelatex" }] - header: [{ - text: "register" - url: "/register" - only_when_logged_out: true - }, { - text: "log_in" - url: "/login" - only_when_logged_out: true - }, { - text: "Projects" - url: "/project" - only_when_logged_in: true - }, { - text: "Account" - only_when_logged_in: true - dropdown: [{ - user_email: true - },{ - divider: true - }, { - text: "Account Settings" - url: "/user/settings" - }, { - divider: true - }, { - text: "Log out" - url: "/logout" - }] - }] + showSubscriptionLink: false + + header_extras: [] + # Example: + # header_extras: [{text: "Some Page", url: "http://example.com/some/page", class: "subdued"}] customisation: {} From 411cb4330c788065669b1a20187841828a77463e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 11 Jan 2017 10:39:34 +0000 Subject: [PATCH 30/31] Fix typo in hyperlink --- services/web/app/views/layout/navbar.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 8ec5b5dbf3..4d78d02fba 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -65,7 +65,7 @@ nav.navbar.navbar-default // projects link and account menu if getSessionUser() li - a(href="/projects") #{translate('Projects')} + a(href="/project") #{translate('Projects')} li.dropdown(dropdown) a.dropbodw-toggle(href, dropdown-toggle) | #{translate('Account')} From df4b5c1b3777764c5404978bb2a1b035e9e8b382 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 12 Jan 2017 13:25:19 +0000 Subject: [PATCH 31/31] math mode syntax checking improvements for beta users from our ace commit 442a1c522c58b1e511d2fd2c6f03909488d41e60 --- .../public/js/ace-1.2.5/worker-latex_beta.js | 221 +++++++++++++++--- 1 file changed, 188 insertions(+), 33 deletions(-) diff --git a/services/web/public/js/ace-1.2.5/worker-latex_beta.js b/services/web/public/js/ace-1.2.5/worker-latex_beta.js index b47d8f0a46..720c3e5009 100644 --- a/services/web/public/js/ace-1.2.5/worker-latex_beta.js +++ b/services/web/public/js/ace-1.2.5/worker-latex_beta.js @@ -1554,6 +1554,25 @@ var read1arg = function (TokeniseResult, k, options) { } }; +var readLetDefinition = function (TokeniseResult, k) { + + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var first = Tokens[k+1]; + var second = Tokens[k+2]; + var third = Tokens[k+3]; + + if(first && first[1] === "\\" && second && second[1] === "\\") { + return k + 2; + } else if(first && first[1] === "\\" && + second && second[1] === "Text" && text.substring(second[2], second[3]) === "=" && + third && third[1] === "\\") { + return k + 3; + } else { + return null; + } +}; var read1name = function (TokeniseResult, k) { var Tokens = TokeniseResult.tokens; @@ -1624,9 +1643,56 @@ var readOptionalParams = function(TokeniseResult, k) { return k + 1; // got it }; }; + var count = 0; + var nextToken = Tokens[k+1]; + var pos = nextToken[2]; + + for (var i = pos, end = text.length; i < end; i++) { + var char = text[i]; + if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; + if (char === "[") { count++; } + if (char === "]") { count--; } + if (count === 0 && char === "{") { return k - 1; } + if (count > 0 && (char === '\r' || char === '\n')) { return null; } + }; return null; }; +var readOptionalGeneric = function(TokeniseResult, k) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var params = Tokens[k+1]; + + if(params && params[1] === "Text") { + var paramNum = text.substring(params[2], params[3]); + if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) { + return k + 1; // got it + }; + }; + return null; +}; + +var readOptionalDef = function (TokeniseResult, k) { + var Tokens = TokeniseResult.tokens; + var text = TokeniseResult.text; + + var defToken = Tokens[k]; + var pos = defToken[3]; + + var openBrace = "{"; + var nextToken = Tokens[k+1]; + for (var i = pos, end = text.length; i < end; i++) { + var char = text[i]; + if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; + if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments + if (char === '\r' || char === '\n') { return null; } + }; + + return null; + +}; + var readDefinition = function(TokeniseResult, k) { var Tokens = TokeniseResult.tokens; var text = TokeniseResult.text; @@ -1726,10 +1792,27 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { var Environments = new EnvHandler(ErrorReporter); var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox) + var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes + var seenUserDefinedBeginEquation = false; // if we have seen macros like \beq + var seenUserDefinedEndEquation = false; // if we have seen macros like \eeq for (var i = 0, len = Tokens.length; i < len; i++) { var token = Tokens[i]; var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4]; + + if (type === "{") { + Environments.push({command:"{", token:token, mathMode: nextGroupMathMode}); + nextGroupMathModeStack.push(nextGroupMathMode); + nextGroupMathMode = null; + continue; + } else if (type === "}") { + Environments.push({command:"}", token:token}); + nextGroupMathMode = nextGroupMathModeStack.pop(); + continue; + } else { + nextGroupMathMode = null; + }; + if (type === "\\") { if (seq === "begin" || seq === "end") { var open = Tokens[i+1]; @@ -1778,15 +1861,31 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { } else { TokenError(token, "invalid environment command"); }; - } - } else if (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") { - var newPos = read1arg(TokeniseResult, i, {allowStar: (seq != "def")}); + } + } else if (typeof seq === "string" && seq.match(/^(be|beq|beqa|bea)$/i)) { + seenUserDefinedBeginEquation = true; + } else if (typeof seq === "string" && seq.match(/^(ee|eeq|eeqn|eeqa|eeqan|eea)$/i)) { + seenUserDefinedEndEquation = true; + } else if (seq === "newcommand" || seq === "renewcommand" || seq === "DeclareRobustCommand") { + var newPos = read1arg(TokeniseResult, i, {allowStar: true}); if (newPos === null) { continue; } else {i = newPos;}; newPos = readOptionalParams(TokeniseResult, i); if (newPos === null) { /* do nothing */ } else {i = newPos;}; newPos = readDefinition(TokeniseResult, i); if (newPos === null) { /* do nothing */ } else {i = newPos;}; + } else if (seq === "def") { + newPos = read1arg(TokeniseResult, i); + if (newPos === null) { continue; } else {i = newPos;}; + newPos = readOptionalDef(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + + } else if (seq === "let") { + newPos = readLetDefinition(TokeniseResult, i); + if (newPos === null) { continue; } else {i = newPos;}; + } else if (seq === "newcolumntype") { newPos = read1name(TokeniseResult, i); if (newPos === null) { continue; } else {i = newPos;}; @@ -1820,7 +1919,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { } else if (nextToken && nextToken[1] === "\\") { char = "unknown"; } - if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) { + if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) { TokenError(token, "invalid bracket command"); } else { i = i + 1; @@ -1831,25 +1930,50 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { } else if (seq === "input") { newPos = read1filename(TokeniseResult, i); if (newPos === null) { continue; } else {i = newPos;}; - } else if (seq === "hbox" || seq === "text" || seq === "mbox") { + } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") { nextGroupMathMode = false; + } else if (seq === "rotatebox" || seq === "scalebox") { + newPos = readOptionalGeneric(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + nextGroupMathMode = false; + } else if (seq === "resizebox") { + newPos = readOptionalGeneric(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + + nextGroupMathMode = false; + } else if (seq === "DeclareMathOperator") { + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + } else if (seq === "DeclarePairedDelimiter") { + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; + newPos = readDefinition(TokeniseResult, i); + if (newPos === null) { /* do nothing */ } else {i = newPos;}; } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) { var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (currentMathMode === null && !insideGroup) { - TokenError(token, type + seq + " must be inside math mode"); + if (currentMathMode === null) { + TokenError(token, type + seq + " must be inside math mode", {mathMode:true}); }; - } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) { + } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) { currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (currentMathMode && !insideGroup) { - TokenError(token, type + seq + " used inside math mode"); + if (currentMathMode) { + TokenError(token, type + seq + " used inside math mode", {mathMode:true}); Environments.resetMathMode(); }; + } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) { + nextGroupMathMode = undefined; }; - } else if (type === "{") { - Environments.push({command:"{", token:token, mathMode: nextGroupMathMode}); - nextGroupMathMode = null; - } else if (type === "}") { - Environments.push({command:"}", token:token}); + } else if (type === "$") { var lookAhead = Tokens[i+1]; var nextIsDollar = lookAhead && lookAhead[1] === "$"; @@ -1864,12 +1988,15 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) var insideGroup = Environments.insideGroup(); // true if inside {....} if (currentMathMode === null && !insideGroup) { - TokenError(token, type + " must be inside math mode"); + TokenError(token, type + " must be inside math mode", {mathMode:true}); }; - } else { - nextGroupMathMode = null; } }; + + if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) { + ErrorReporter.filterMath = true; + }; + return Environments; }; @@ -1920,7 +2047,7 @@ var EnvHandler = function (ErrorReporter) { if (documentClosed) { ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); } else { - ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}"); + ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); } } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) { invalidEnvs.splice(i, 1); @@ -2054,7 +2181,7 @@ var EnvHandler = function (ErrorReporter) { var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env if (currentMathMode) { ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true}); + {suppressIfEditing:true, errorAtStart: true, mathMode:true}); }; thisEnv.mathMode = thisEnv; state.push(thisEnv); @@ -2118,28 +2245,28 @@ var EnvHandler = function (ErrorReporter) { } } else if (thisEnv.command === "left") { if (currentMathMode === null) { - ErrorFrom(thisEnv, "\\left can only be used in math mode"); + ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true}); }; newMathMode = currentMathMode; } else if (thisEnv.command === "begin") { var name = thisEnv.name; if (name) { - if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { + if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { if (currentMathMode) { ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true}); + {suppressIfEditing:true, errorAtStart: true, mathMode: true}); resetMathMode(); }; newMathMode = null; - } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) { - if (!currentMathMode) { - ErrorFrom(thisEnv, thisEnv.name + " not inside math mode"); + } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) { + if (currentMathMode === null) { + ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true}); }; newMathMode = currentMathMode; } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) { if (currentMathMode) { ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true}); + {suppressIfEditing:true, errorAtStart: true, mathMode: true}); resetMathMode(); }; newMathMode = thisEnv; @@ -2220,13 +2347,36 @@ var ErrorReporter = function (TokeniseResult) { var errors = [], tokenErrors = []; this.errors = errors; this.tokenErrors = tokenErrors; + this.filterMath = false; this.getErrors = function () { var returnedErrors = []; for (var i = 0, len = tokenErrors.length; i < len; i++) { if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); } } - return returnedErrors.concat(errors); + var allErrors = returnedErrors.concat(errors); + var result = []; + + var mathErrorCount = 0; + for (i = 0, len = allErrors.length; i < len; i++) { + if (allErrors[i].mathMode) { + mathErrorCount++; + } + if (mathErrorCount > 10) { + return []; + } + } + + if (this.filterMath && mathErrorCount > 0) { + for (i = 0, len = allErrors.length; i < len; i++) { + if (!allErrors[i].mathMode) { + result.push(allErrors[i]); + } + } + return result; + } else { + return allErrors; + } }; this.TokenError = function (token, message, options) { @@ -2245,7 +2395,8 @@ var ErrorReporter = function (TokeniseResult) { text:message, startPos: start, endPos: end, - suppressIfEditing:options.suppressIfEditing}); + suppressIfEditing:options.suppressIfEditing, + mathMode: options.mathMode}); }; this.TokenErrorFromTo = function (fromToken, toToken, message, options) { @@ -2266,7 +2417,8 @@ var ErrorReporter = function (TokeniseResult) { text:message, startPos: fromStart, endPos: toEnd, - suppressIfEditing:options.suppressIfEditing}); + suppressIfEditing:options.suppressIfEditing, + mathMode: options.mathMode}); }; @@ -2287,7 +2439,8 @@ var ErrorReporter = function (TokeniseResult) { end_col: end_col, type: options.type ? options.type : "error", text:message, - suppressIfEditing:options.suppressIfEditing}); + suppressIfEditing:options.suppressIfEditing, + mathMode: options.mathMode}); }; this.EnvErrorTo = function (toEnv, message, options) { @@ -2303,7 +2456,8 @@ var ErrorReporter = function (TokeniseResult) { end_row: line, end_col: end_col, type: options.type ? options.type : "error", - text:message}; + text:message, + mathMode: options.mathMode}; errors.push(err); }; @@ -2320,7 +2474,8 @@ var ErrorReporter = function (TokeniseResult) { end_row: lineNumber, end_col: end_col, type: options.type ? options.type : "error", - text:message}); + text:message, + mathMode: options.mathMode}); }; };