diff --git a/services/web/.gitignore b/services/web/.gitignore index d7b110e28d..9c2a272a7a 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -60,6 +60,7 @@ public/js/services/ public/js/utils/ public/stylesheets/style.css +public/stylesheets/ol-style.css public/brand/plans.css public/minjs/ diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 45a6528cdd..479190332f 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -139,6 +139,9 @@ module.exports = (grunt) -> app: files: "public/stylesheets/style.css": "public/stylesheets/style.less" + ol: + files: + "public/stylesheets/ol-style.css": "public/stylesheets/ol-style.less" postcss: options: diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile new file mode 100644 index 0000000000..bee222f13b --- /dev/null +++ b/services/web/Jenkinsfile @@ -0,0 +1,116 @@ +pipeline { + + agent { + docker { + image 'node:6.9.5' + args "-v /var/lib/jenkins/.npm:/tmp/.npm" + } + } + + environment { + HOME = "/tmp" + } + + triggers { + pollSCM('* * * * *') + cron('@daily') + } + + stages { + stage('Set up') { + steps { + // we need to disable logallrefupdates, else git clones during the npm install will require git to lookup the user id + // which does not exist in the container's /etc/passwd file, causing the clone to fail. + sh 'git config --global core.logallrefupdates false' + + sh 'rm -rf node_modules/*' + } + } + + stage('Clone Dependencies') { + steps { + sh 'rm -rf public/brand modules' + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'public/brand'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/brand-sharelatex']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'app/views/external'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/external-pages-sharelatex']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/web-sharelatex-modules']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/admin-panel'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/admin-panel']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/groovehq'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/groovehq']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/references-search'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/references-search.git']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/tpr-webmodule'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/tpr-webmodule.git ']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]]) + } + } + + stage('Install') { + steps { + sh 'mv app/views/external/robots.txt public/robots.txt' + sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html' + sh 'npm install' + sh 'npm rebuild' + sh 'npm install --quiet grunt' + sh 'npm install --quiet grunt-cli' + sh 'ls -l node_modules/.bin' + } + } + + stage('Compile') { + steps { + sh 'node_modules/.bin/grunt compile --verbose' + } + } + + stage('Smoke Test') { + steps { + sh 'node_modules/.bin/grunt compile:smoke_tests' + } + } + + stage('Minify') { + steps { + sh 'node_modules/.bin/grunt compile:minify' + } + } + + stage('Unit Test') { + steps { + sh 'env NODE_ENV=development ./node_modules/.bin/grunt test:unit --reporter=tap' + } + } + + stage('Package') { + steps { + sh 'rm -rf ./node_modules/grunt*' + sh 'touch build.tar.gz' // Avoid tar warning about files changing during read + sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .' + } + } + stage('Publish') { + steps { + withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") { + s3Upload(file:'build.tar.gz', bucket:"${S3_BUCKET_BUILD_ARTEFACTS}", path:"${JOB_NAME}/${BUILD_NUMBER}.tar.gz") + } + } + } + } + + post { + failure { + mail(from: "${EMAIL_ALERT_FROM}", + to: "${EMAIL_ALERT_TO}", + subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}", + body: "Build: ${BUILD_URL}") + } + } + + + // The options directive is for configuration that applies to the whole job. + options { + // we'd like to make sure remove old builds, so we don't fill up our storage! + buildDiscarder(logRotator(numToKeepStr:'50')) + + // And we'd really like to be sure that this build doesn't hang forever, so let's time it out after: + timeout(time: 30, unit: 'MINUTES') + } +} diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee index b661455028..49bd994b2c 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -29,8 +29,12 @@ module.exports = AuthenticationManager = callback null, null setUserPassword: (user_id, password, callback = (error) ->) -> - if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length + if (Settings.passwordStrengthOptions?.length?.max? and + Settings.passwordStrengthOptions?.length?.max < password.length) return callback("password is too long") + if (Settings.passwordStrengthOptions?.length?.min? and + Settings.passwordStrengthOptions?.length?.min > password.length) + return callback("password is too short") bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 193965c101..84fa89ea37 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -230,6 +230,24 @@ module.exports = ProjectController = return cb(null, false) else return cb(null, true) + showPerUserTCNotice: (cb) -> + cb = underscore.once(cb) + if !user_id? + return cb() + timestamp = user_id.toString().substring(0,8) + userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 ) + if userSignupDate > new Date("2017-08-09") + # Don't show for users who registered after it was released + return cb(null, false) + timeout = setTimeout cb, 500 + AnalyticsManager.getLastOccurance user_id, "shown-per-user-tc-notice", (error, event) -> + clearTimeout timeout + if error? + return cb(null, false) + else if event? + return cb(null, false) + else + return cb(null, true) }, (err, results)-> if err? logger.err err:err, "error getting details for project page" @@ -237,7 +255,7 @@ module.exports = ProjectController = project = results.project user = results.user subscription = results.subscription - showTrackChangesOnboarding = results.showTrackChangesOnboarding + { showTrackChangesOnboarding, showPerUserTCNotice } = results daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" @@ -279,8 +297,9 @@ module.exports = ProjectController = pdfViewer : user.ace.pdfViewer syntaxValidation: user.ace.syntaxValidation } - trackChangesEnabled: !!project.track_changes + trackChangesState: project.track_changes showTrackChangesOnboarding: !!showTrackChangesOnboarding + showPerUserTCNotice: !!showPerUserTCNotice privilegeLevel: privilegeLevel chatUrl: Settings.apis.chat.url anonymous: anonymous diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 7b857c6460..12faf0e234 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -62,6 +62,15 @@ module.exports = SubscriptionUpdater = invited_emails: email }, callback + deleteSubscription: (subscription_id, callback = (error) ->) -> + SubscriptionLocator.getSubscription subscription_id, (err, subscription) -> + return callback(err) if err? + affected_user_ids = [subscription.admin_id].concat(subscription.member_ids or []) + logger.log {subscription_id, affected_user_ids}, "deleting subscription and downgrading users" + Subscription.remove {_id: ObjectId(subscription_id)}, (err) -> + return callback(err) if err? + async.mapSeries affected_user_ids, SubscriptionUpdater._setUsersMinimumFeatures, callback + _createNewSubscription: (adminUser_id, callback)-> logger.log adminUser_id:adminUser_id, "creating new subscription" subscription = new Subscription(admin_id:adminUser_id) diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee index d4ce82cafb..d08b953559 100644 --- a/services/web/app/coffee/Features/User/UserCreator.coffee +++ b/services/web/app/coffee/Features/User/UserCreator.coffee @@ -17,23 +17,15 @@ module.exports = UserCreator = createNewUser: (opts, callback)-> logger.log opts:opts, "creating new user" user = new User() - user.email = opts.email - user.holdingAccount = opts.holdingAccount - user.ace.syntaxValidation = true username = opts.email.match(/^[^@]*/) - if opts.first_name? and opts.first_name.length != 0 - user.first_name = opts.first_name - else if username? - user.first_name = username[0] - else - user.first_name = "" - - if opts.last_name? - user.last_name = opts.last_name - else - user.last_name = "" + if !opts.first_name? or opts.first_name == "" + opts.first_name = username[0] + for key, value of opts + user[key] = value + + user.ace.syntaxValidation = true user.featureSwitches?.pdfng = true user.save (err)-> diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 4c6b3a7722..498127cdbd 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -11,6 +11,7 @@ async = require("async") Modules = require "./Modules" Url = require "url" PackageVersions = require "./PackageVersions" +htmlEncoder = new require("node-html-encoder").Encoder("numerical") fingerprints = {} Path = require 'path' @@ -151,9 +152,10 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> next() webRouter.use (req, res, next)-> - res.locals.translate = (key, vars = {}) -> + res.locals.translate = (key, vars = {}, htmlEncode = false) -> vars.appName = Settings.appName - req.i18n.translate(key, vars) + str = req.i18n.translate(key, vars) + if htmlEncode then htmlEncoder.htmlEncode(str) else str # Don't include the query string parameters, otherwise Google # treats ?nocdn=true as the canonical version res.locals.currentUrl = Url.parse(req.originalUrl).pathname diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 18387bdc0b..c63f9efef3 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -32,7 +32,7 @@ ProjectSchema = new Schema archived : { type: Boolean } deletedDocs : [DeletedDocSchema] imageName : { type: String } - track_changes : { type: Boolean } + track_changes : { type: Object } ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> if project_or_id._id? diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index fce4b18bc0..4dfea8b9cf 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -59,6 +59,10 @@ UserSchema = new Schema zotero: Boolean } betaProgram: { type:Boolean, default: false} + overleaf: + id: { type: Number } + accessToken: { type: String } + refreshToken: { type: String } conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10) diff --git a/services/web/app/views/contact-us-modal.pug b/services/web/app/views/contact-us-modal.pug index d3e5aa0e87..aad68a53d3 100644 --- a/services/web/app/views/contact-us-modal.pug +++ b/services/web/app/views/contact-us-modal.pug @@ -7,36 +7,61 @@ script(type='text/ng-template', id='supportModalTemplate') ) × h3 #{translate("contact_us")} .modal-body.contact-us-modal - span(ng-show="sent == false") - label - | #{translate("subject")} - .form-group - input.field.text.medium.span8.form-control( - ng-model="form.subject", - ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }" - maxlength='255', - tabindex='1', - onkeyup='') - .contact-suggestions(ng-show="suggestions.length") - p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "__kb__", kb: translate("knowledge_base") })} - ul.contact-suggestion-list - li(ng-repeat="suggestion in suggestions") - a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") - span(ng-bind-html="suggestion.name") - i.fa.fa-angle-right - label.desc(ng-show="'"+getUserEmail()+"'.length < 1") - | #{translate("email")} - .form-group(ng-show="'"+getUserEmail()+"'.length < 1") - input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') - label#title12.desc - | #{translate("project_url")} (#{translate("optional")}) - .form-group - input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') - label.desc - | #{translate("contact_message_label")} - .form-group - textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='') - .form-group.text-center - input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us")) + form(name="contactForm") + span(ng-show="sent == false") + .alert.alert-danger(ng-show="error") Something went wrong sending your request :( + label + | #{translate("subject")} + .form-group + input.field.text.medium.span8.form-control( + name="subject", + required + ng-model="form.subject", + ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }" + maxlength='255', + tabindex='1', + onkeyup='') + .contact-suggestions(ng-show="suggestions.length") + p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "__kb__", kb: translate("knowledge_base") })} + ul.contact-suggestion-list + li(ng-repeat="suggestion in suggestions") + a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") + span(ng-bind-html="suggestion.name") + i.fa.fa-angle-right + label.desc(ng-show="'"+getUserEmail()+"'.length < 1") + | #{translate("email")} + .form-group(ng-show="'"+getUserEmail()+"'.length < 1") + input.field.text.medium.span8.form-control( + name="email", + required + ng-model="form.email", + ng-init="form.email = '"+getUserEmail()+"'", + type='email', spellcheck='false', + value='', + maxlength='255', + tabindex='2') + label#title12.desc + | #{translate("project_url")} (#{translate("optional")}) + .form-group + input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') + label.desc + | #{translate("contact_message_label")} + .form-group + textarea.field.text.medium.span8.form-control( + name="body", + required + ng-model="form.message", + type='text', + value='', + tabindex='4', + onkeyup='' + ) + .form-group.text-center + input.btn-success.btn.btn-lg( + type='submit', + ng-disabled="contactForm.$invalid || sending", + ng-click="contactUs()" + value=translate("contact_us") + ) span(ng-show="sent") p #{translate("request_sent_thank_you")} diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 459f081c8a..30f454f72a 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -14,16 +14,16 @@ html(itemscope, itemtype='http://schema.org/Product') -if (typeof(title) == "undefined") - title= 'ShareLaTeX, '+ translate("online_latex_editor") + title= settings.appName + ', '+ translate("online_latex_editor") -else - title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor") + title= translate(title) + ' - ' + settings.appName + ', ' + translate("online_latex_editor") - link(rel="icon", href="/favicon.ico") - link(rel="icon", sizes="192x192", href="/touch-icon-192x192.png") - link(rel="apple-touch-icon-precomposed", href="/apple-touch-icon-precomposed.png") - link(rel="mask-icon", href="/mask-favicon.svg", color="#a93529") + link(rel="icon", href="/" + settings.brandPrefix + "favicon.ico") + link(rel="icon", sizes="192x192", href="/" + settings.brandPrefix + "touch-icon-192x192.png") + link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png") + link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529") - link(rel='stylesheet', href=buildCssPath('/style.css')) + link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css")) block _headLinks @@ -33,14 +33,14 @@ html(itemscope, itemtype='http://schema.org/Product') link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode) - meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor") + meta(itemprop="name", content=settings.appName + ", the Online LaTeX Editor") -if (typeof(meta) == "undefined") meta(itemprop="description", name="description", content=translate("site_description")) -else meta(itemprop="description", name="description" , content=meta) - meta(itemprop="image", name="image", content="https://www.sharelatex.com/favicon.ico") + meta(itemprop="image", name="image", content="/" + settings.brandPrefix + "favicon.ico") - if (typeof(gaToken) != "undefined") script(type='text/javascript'). diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 5b6137738b..190a46b77e 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -8,8 +8,8 @@ block vars block content .editor(ng-controller="IdeController").full-size .loading-screen(ng-if="state.loading") - .loading-screen-lion-container - .loading-screen-lion( + .loading-screen-brand-container + .loading-screen-brand( style="height: 20%;" ng-style="{ 'height': state.load_progress + '%' }" ) @@ -106,7 +106,7 @@ block requirejs //- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>' //- and doesn't prematurely end the script tag. script#data(type="application/json"). - !{JSON.stringify({userSettings: userSettings, user: user}).replace(/\//g, '\\/')} + !{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')} script(type="text/javascript"). window.data = JSON.parse($("#data").text()); @@ -118,8 +118,9 @@ block requirejs window.csrfToken = "!{csrfToken}"; window.anonymous = #{anonymous}; window.maxDocLength = #{maxDocLength}; - window.trackChangesEnabled = #{trackChangesEnabled}; + window.trackChangesState = data.trackChangesState; window.showTrackChangesOnboarding = #{!!showTrackChangesOnboarding}; + window.showPerUserTCNotice = #{!!showPerUserTCNotice}; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 8b58c98bcd..6007f2e0be 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -82,7 +82,7 @@ div.full-size( i.fa.fa-long-arrow-right br a.btn.btn-default.btn-xs( - tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'" + tooltip-html="'"+translate('go_to_pdf_location_in_code', {}, true)+"'" tooltip-placement="right" tooltip-append-to-body="true" ng-click="syncToCode()" diff --git a/services/web/app/views/project/editor/hotkeys.pug b/services/web/app/views/project/editor/hotkeys.pug index cab8ea09b3..6069394840 100644 --- a/services/web/app/views/project/editor/hotkeys.pug +++ b/services/web/app/views/project/editor/hotkeys.pug @@ -90,8 +90,8 @@ script(type="text/ng-template", id="hotkeysModalTemplate") span.combination Ctrl + Space span.description Search References - h3 #{translate("review")} - .row + h3(ng-if="trackChangesVisible") #{translate("review")} + .row(ng-if="trackChangesVisible") .col-xs-4 .hotkey span.combination {{ctrl}} + J @@ -108,4 +108,4 @@ script(type="text/ng-template", id="hotkeysModalTemplate") .modal-footer button.btn.btn-default( ng-click="cancel()" - ) #{translate("ok")} \ No newline at end of file + ) #{translate("ok")} diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug index 07f6bd081e..6db177eba8 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -46,21 +46,40 @@ is-loading="reviewPanel.dropdown.loading" permissions="permissions" ) - span.review-panel-toolbar-label(ng-if="permissions.write") - span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} - span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} - review-panel-toggle( - ng-if="editor.wantTrackChanges == editor.trackChanges" - ng-model="editor.wantTrackChanges" - on-toggle="toggleTrackChanges" - disabled="!project.features.trackChanges" - on-disabled-click="openTrackChangesUpgradeModal" + span.review-panel-toolbar-label + span.review-panel-toolbar-icon-on( + ng-if="editor.wantTrackChanges === true" ) - span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write") - span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} - span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} - span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges") - i.fa.fa-spin.fa-spinner + i.fa.fa-circle + span(ng-click="toggleFullTCStateCollapse();") + span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} + span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} + span.rp-tc-state-collapse( + ng-class="{ 'rp-tc-state-collapse-on': reviewPanel.fullTCStateCollapsed }" + ) + i.fa.fa-angle-down + ul.rp-tc-state( + review-panel-collapse-height="reviewPanel.fullTCStateCollapsed" + ) + li.rp-tc-state-item.rp-tc-state-item-everyone + span.rp-tc-state-item-name !{translate("tc_everyone")} + review-panel-toggle( + ng-model="reviewPanel.trackChangesOnForEveryone" + on-toggle="toggleTrackChangesForEveryone(isOn);" + disabled="!project.features.trackChanges || !permissions.write" + ) + li.rp-tc-state-item( + ng-repeat="member in reviewPanel.formattedProjectMembers" + ) + span.rp-tc-state-item-name( + ng-class="{ 'rp-tc-state-item-name-disabled' : reviewPanel.trackChangesOnForEveryone}" + style="color: hsl({{ member.hue }}, 70%, 40%);" + ) {{ member.name }} + review-panel-toggle( + ng-model="reviewPanel.trackChangesState[member.id].value" + on-toggle="toggleTrackChangesForUser(isOn, member.id);" + disabled="reviewPanel.trackChangesOnForEveryone || !project.features.trackChanges || !permissions.write" + ) .rp-entry-list( review-panel-sorted @@ -494,7 +513,7 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate") .modal-body .teaser-video-container video.teaser-video(autoplay, loop) - source(src="/img/teasers/track-changes/teaser-track-changes.mp4", type="video/mp4") + source(ng-src="{{ '/img/teasers/track-changes/teaser-track-changes.mp4' }}", type="video/mp4") img(src="/img/teasers/track-changes/teaser-track-changes.gif") h4.teaser-title #{translate("see_changes_in_your_documents_live")} @@ -532,6 +551,37 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate") ) span #{translate("close")} +script(type="text/ng-template", id="perUserTCNoticeModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="$close()" + ) × + h3 #{translate("per_user_tc_title")} + .modal-body + .teaser-video-container + video.teaser-video(autoplay, loop) + source(ng-src="{{ '/img/teasers/track-changes/per-user-track-changes.mp4' }}", type="video/mp4") + img(src="/img/teasers/track-changes/per-user-track-changes.gif") + h4.teaser-title #{translate("you_can_use_per_user_tc")} + .row + .col-md-8.col-md-offset-2 + ul.list-unstyled + li + i.fa.fa-check   + | #{translate("turn_tc_on_individuals")} + + li + i.fa.fa-check   + | #{translate("keep_tc_on_like_before")} + + .modal-footer() + button.btn.btn-default( + ng-click="$close()" + ) + span #{translate("close")} + script(type="text/ng-template", id="bulkActionsModalTemplate") .modal-header button.close( diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 8fb00aff31..2456590709 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -226,8 +226,8 @@ module.exports = settings = # passwordStrengthOptions: # pattern: "aA$3" # length: - # min: 1 - # max: 10 + # min: 6 + # max: 128 # Email support # ------------- @@ -343,6 +343,8 @@ module.exports = settings = appName: "ShareLaTeX (Community Edition)" adminEmail: "placeholder@example.com" + + brandPrefix: "" # Set to 'ol-' for overleaf styles nav: title: "ShareLaTeX Community Edition" diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json index a8357be7fd..042a93f8af 100644 --- a/services/web/npm-shrinkwrap.json +++ b/services/web/npm-shrinkwrap.json @@ -1509,7 +1509,7 @@ }, "minimatch": { "version": "3.0.4", - "from": "minimatch@^3.0.4", + "from": "minimatch@^3.0.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "dev": true }, @@ -1678,7 +1678,7 @@ }, "iconv-lite": { "version": "0.2.11", - "from": "iconv-lite@>=0.2.11 <0.3.0", + "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" }, "ieee754": { @@ -1698,7 +1698,7 @@ }, "inherits": { "version": "2.0.3", - "from": "inherits@>=2.0.1 <2.1.0", + "from": "inherits@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" }, "ini": { @@ -2728,6 +2728,11 @@ "from": "node-forge@0.2.24", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.2.24.tgz" }, + "node-html-encoder": { + "version": "0.0.2", + "from": "node-html-encoder@0.0.2", + "resolved": "https://registry.npmjs.org/node-html-encoder/-/node-html-encoder-0.0.2.tgz" + }, "node-pre-gyp": { "version": "0.6.30", "from": "node-pre-gyp@0.6.30", @@ -2842,6 +2847,11 @@ "from": "number-is-nan@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" }, + "oauth": { + "version": "0.9.15", + "from": "oauth@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz" + }, "oauth-sign": { "version": "0.8.2", "from": "oauth-sign@>=0.8.1 <0.9.0", @@ -3065,6 +3075,11 @@ "from": "passport-local@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz" }, + "passport-oauth2": { + "version": "1.4.0", + "from": "passport-oauth2@latest", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz" + }, "passport-saml": { "version": "0.15.0", "from": "passport-saml@>=0.15.0 <0.16.0", @@ -4287,6 +4302,11 @@ "from": "uid-safe@2.1.4", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz" }, + "uid2": { + "version": "0.0.3", + "from": "uid2@>=0.0.0 <0.1.0", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz" + }, "underscore": { "version": "1.6.0", "from": "underscore@1.6.0", diff --git a/services/web/package.json b/services/web/package.json index eac07a06e5..2c3dab21d8 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -41,6 +41,7 @@ "mongojs": "2.4.0", "mongoose": "4.11.4", "multer": "^0.1.8", + "node-html-encoder": "0.0.2", "nodemailer": "2.1.0", "nodemailer-sendgrid-transport": "^0.2.0", "nodemailer-ses-transport": "^1.3.0", @@ -48,6 +49,7 @@ "passport": "^0.3.2", "passport-ldapauth": "^0.6.0", "passport-local": "^1.0.0", + "passport-oauth2": "^1.4.0", "passport-saml": "^0.15.0", "pug": "^2.0.0-beta6", "redis": "0.10.1", diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index 2fc336c934..0e6ae19ec2 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -103,8 +103,8 @@ define [ defaultPasswordOpts = pattern: "" length: - min: 1 - max: 50 + min: 6 + max: 128 allowEmpty: false allowAnyChars: false isMasked: true @@ -127,8 +127,6 @@ define [ [asyncFormCtrl, ngModelCtrl] = ctrl ngModelCtrl.$parsers.unshift (modelValue) -> - - isValid = passField.validatePass() email = asyncFormCtrl.getEmail() || window.usersEmail if !isValid @@ -141,5 +139,8 @@ define [ if opts.length.max? and modelValue.length == opts.length.max isValid = false scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached" + if opts.length.min? and modelValue.length < opts.length.min + isValid = false + scope.complexPasswordErrorMessage = "Password too short, minimum #{opts.length.min}" ngModelCtrl.$setValidity('complexPassword', isValid) return modelValue diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 5824304bff..d37334649c 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -90,6 +90,8 @@ define [ $scope.chat = {} ide.toggleReviewPanel = $scope.toggleReviewPanel = () -> + if !$scope.project.features.trackChangesVisible + return $scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen event_tracking.sendMB "rp-toggle-panel", { value : $scope.ui.reviewPanelOpen } diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index a1d00d3180..bf752ccffb 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -37,10 +37,6 @@ define [ @$scope.$watch "editor.wantTrackChanges", (value) => return if !value? @_syncTrackChangesState(@$scope.editor.sharejs_doc) - - @$scope.$watch "project.features.trackChanges", (trackChangesFeature) => - return if !trackChangesFeature? - @$scope.editor.wantTrackChanges = window.trackChangesEnabled and trackChangesFeature autoOpenDoc: () -> open_doc_id = diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index af633ea369..1cb527fc40 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -11,6 +11,8 @@ define [ "ide/editor/directives/aceEditor/track-changes/TrackChangesManager" "ide/editor/directives/aceEditor/labels/LabelsManager" "ide/labels/services/labels" + "ide/graphics/services/graphics" + "ide/preamble/services/preamble" ], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) -> EditSession = ace.require('ace/edit_session').EditSession ModeList = ace.require('ace/ext/modelist') @@ -33,9 +35,8 @@ define [ url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}" return url - App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels) -> + App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, graphics, preamble) -> monkeyPatchSearch($rootScope, $compile) - return { scope: { @@ -102,7 +103,7 @@ define [ cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) trackChangesManager = new TrackChangesManager(scope, editor, element) labelsManager = new LabelsManager(scope, editor, element, labels) - autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager) + autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager, graphics, preamble) # Prevert Ctrl|Cmd-S from triggering save dialog @@ -346,10 +347,6 @@ define [ catch mode = "ace/mode/plain_text" - # Give beta users the next release of the syntax checker - if mode is "ace/mode/latex" and window.user?.betaProgram - mode = "ace/mode/latex_beta" - # create our new session session = new EditSession(lines, mode) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 4466f200c6..55104341e8 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -17,7 +17,7 @@ define [ commandFragment?.match(/\\(\w+)\{/)?[1] class AutoCompleteManager - constructor: (@$scope, @editor, @element, @labelsManager) -> + constructor: (@$scope, @editor, @element, @labelsManager, @graphics, @preamble) -> @suggestionManager = new CommandManager() @monkeyPatchAutocomplete() @@ -44,6 +44,37 @@ define [ SnippetCompleter = new EnvironmentManager() + Graphics = @graphics + Preamble = @preamble + GraphicsCompleter = + getCompletions: (editor, session, pos, prefix, callback) -> + upToCursorRange = new Range(pos.row, 0, pos.row, pos.column) + lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) + commandFragment = getLastCommandFragment(lineUpToCursor) + if commandFragment + match = commandFragment.match(/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/) + if match + beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) + lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) + needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/) + commandName = match[1] + currentArg = match[3] + graphicsPaths = Preamble.getGraphicsPaths() + result = [] + for graphic in Graphics.getGraphicsFiles() + path = graphic.path + for graphicsPath in graphicsPaths + if path.indexOf(graphicsPath) == 0 + path = path.slice(graphicsPath.length) + break + result.push { + caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}", + value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}", + meta: "graphic", + score: 50 + } + callback null, result + labelsManager = @labelsManager LabelsCompleter = getCompletions: (editor, session, pos, prefix, callback) -> @@ -113,11 +144,12 @@ define [ callback null, result @editor.completers = [ - @suggestionManager, - SnippetCompleter, - ReferencesCompleter, - LabelsCompleter - ] + @suggestionManager, + SnippetCompleter, + ReferencesCompleter, + LabelsCompleter, + GraphicsCompleter + ] disable: () -> @editor.setOptions({ @@ -250,7 +282,22 @@ define [ editor.completer.autoSelect = true editor.completer.showPopup(editor) editor.completer.cancelContextMenu() - $(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'}) + container = $(editor.completer.popup?.container) + container.css({'font-size': @$scope.fontSize + 'px'}) + # Dynamically set width of autocomplete popup + if filtered = editor?.completer?.completions?.filtered + longestCaption = _.max(filtered.map( (c) -> c.caption.length )) + longestMeta = _.max(filtered.map( (c) -> c.meta.length )) + charWidth = editor.renderer.characterWidth + # between 280 and 700 px + width = Math.max( + Math.min( + Math.round(longestCaption*charWidth + longestMeta*charWidth + 5*charWidth), + 700 + ), + 280 + ) + container.css({width: "#{width}px"}) if editor.completer?.completions?.filtered?.length == 0 editor.completer.detach() bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space" diff --git a/services/web/public/coffee/ide/graphics/services/graphics.coffee b/services/web/public/coffee/ide/graphics/services/graphics.coffee new file mode 100644 index 0000000000..355614b7f4 --- /dev/null +++ b/services/web/public/coffee/ide/graphics/services/graphics.coffee @@ -0,0 +1,17 @@ +define [ + "base" +], (App) -> + + App.factory 'graphics', (ide) -> + + Graphics = + getGraphicsFiles: () -> + graphicsFiles = [] + ide.fileTreeManager.forEachEntity (entity, folder, path) -> + if entity.type == 'file' && entity?.name?.match?(/.*\.(png|jpg|jpeg|pdf|eps)/) + cloned = _.clone(entity) + cloned.path = path + graphicsFiles.push cloned + return graphicsFiles + + return Graphics diff --git a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee index 0ec2c46cf1..65b513eda2 100644 --- a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee +++ b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee @@ -10,13 +10,16 @@ define [ templateUrl: "hotkeysModalTemplate" controller: "HotkeysModalController" size: "lg" + resolve: + trackChangesVisible: () -> $scope.project.features.trackChangesVisible } - App.controller "HotkeysModalController", ($scope, $modalInstance)-> + App.controller "HotkeysModalController", ($scope, $modalInstance, trackChangesVisible)-> + $scope.trackChangesVisible = trackChangesVisible if ace.require("ace/lib/useragent").isMac $scope.ctrl = "Cmd" else $scope.ctrl = "Ctrl" $scope.cancel = () -> - $modalInstance.dismiss() \ No newline at end of file + $modalInstance.dismiss() diff --git a/services/web/public/coffee/ide/preamble/services/preamble.coffee b/services/web/public/coffee/ide/preamble/services/preamble.coffee new file mode 100644 index 0000000000..f95cfd9bbe --- /dev/null +++ b/services/web/public/coffee/ide/preamble/services/preamble.coffee @@ -0,0 +1,22 @@ +define [ + "base" +], (App) -> + + App.factory 'preamble', (ide) -> + + Preamble = + getPreambleText: () -> + text = ide.editorManager.getCurrentDocValue().slice(0, 5000) + preamble = text.match(/([^]*)^\\begin\{document\}/m)?[1] || "" + return preamble + + getGraphicsPaths: () -> + preamble = Preamble.getPreambleText() + graphicsPathsArgs = preamble.match(/\\graphicspath\{(.*)\}/)?[1] || "" + paths = [] + re = /\{([^}]*)\}/g + while match = re.exec(graphicsPathsArgs) + paths.push(match[1]) + return paths + + return Preamble diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index e4d7855ed9..67cbedb3e8 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -11,7 +11,13 @@ define [ CUR_FILE : "cur_file" OVERVIEW : "overview" + $scope.UserTCSyncState = UserTCSyncState = + SYNCED : "synced" + PENDING : "pending" + $scope.reviewPanel = + trackChangesState: {} + trackChangesOnForEveryone: false entries: {} resolvedComments: {} hasEntries: false @@ -25,6 +31,8 @@ define [ commentThreads: {} resolvedThreadIds: {} rendererData: {} + formattedProjectMembers: {} + fullTCStateCollapsed: true loadingThreads: false # All selected changes. If a aggregated change (insertion + deletion) is selection, the two ids # will be present. The length of this array will differ from the count below (see explanation). @@ -32,6 +40,7 @@ define [ # A count of user-facing selected changes. An aggregated change (insertion + deletion) will count # as only one. nVisibleSelectedChanges: 0 + showPerUserTCNotice: window.showPerUserTCNotice window.addEventListener "beforeunload", () -> collapsedStates = {} @@ -59,6 +68,15 @@ define [ if !visible $scope.ui.reviewPanelOpen = false + $scope.$watch "project.members", (members) -> + $scope.reviewPanel.formattedProjectMembers = {} + if $scope.project?.owner? + $scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] = formatUser($scope.project.owner) + if $scope.project?.members? + for member in members + if member.privileges == "readAndWrite" + $scope.reviewPanel.formattedProjectMembers[member._id] = formatUser(member) + $scope.commentState = adding: false content: "" @@ -416,6 +434,8 @@ define [ $scope.toggleReviewPanel() $scope.addNewCommentFromKbdShortcut = () -> + if !$scope.project.features.trackChangesVisible + return $scope.$broadcast "comment:select_line" if !$scope.ui.reviewPanelOpen $scope.toggleReviewPanel() @@ -565,24 +585,83 @@ define [ $scope.gotoEntry = (doc_id, entry) -> ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) - - $scope.toggleTrackChanges = (value) -> + + $scope.toggleFullTCStateCollapse = () -> if $scope.project.features.trackChanges - $scope.editor.wantTrackChanges = value - $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value} - event_tracking.sendMB "rp-trackchanges-toggle", { value } + if $scope.reviewPanel.showPerUserTCNotice + $scope.openPerUserTCNoticeModal() + $scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed else $scope.openTrackChangesUpgradeModal() - $scope.toggleTrackChangesFromKbdShortcut = () -> - if $scope.editor.wantTrackChanges - $scope.toggleTrackChanges false - else - $scope.toggleTrackChanges true + _setUserTCState = (userId, newValue, isLocal = false) -> + $scope.reviewPanel.trackChangesState[userId] ?= {} + state = $scope.reviewPanel.trackChangesState[userId] + + if !state.syncState? or state.syncState == UserTCSyncState.SYNCED + state.value = newValue + state.syncState = UserTCSyncState.SYNCED + else if state.syncState == UserTCSyncState.PENDING and state.value == newValue + state.syncState = UserTCSyncState.SYNCED + else if isLocal + state.value = newValue + state.syncState = UserTCSyncState.PENDING + + if userId == ide.$scope.user.id + $scope.editor.wantTrackChanges = newValue + + _setEveryoneTCState = (newValue, isLocal = false) -> + $scope.reviewPanel.trackChangesOnForEveryone = newValue + for member in $scope.project.members + _setUserTCState(member._id, newValue, isLocal) + _setUserTCState($scope.project.owner._id, newValue, isLocal) + + applyClientTrackChangesStateToServer = () -> + if $scope.reviewPanel.trackChangesOnForEveryone + data = {on : true} + else + data = {on_for: {}} + for userId, userState of $scope.reviewPanel.trackChangesState + data.on_for[userId] = userState.value + data._csrf = window.csrfToken + $http.post "/project/#{$scope.project_id}/track_changes", data + + applyTrackChangesStateToClient = (state) -> + if typeof state is "boolean" + _setEveryoneTCState state + else + $scope.reviewPanel.trackChangesOnForEveryone = false + for member in $scope.project.members + _setUserTCState(member._id, state[member._id] ? false) + _setUserTCState($scope.project.owner._id, state[$scope.project.owner._id] ? false) - ide.socket.on "toggle-track-changes", (value) -> + $scope.toggleTrackChangesForEveryone = (onForEveryone) -> + _setEveryoneTCState onForEveryone, true + applyClientTrackChangesStateToServer() + + $scope.toggleTrackChangesForUser = (onForUser, userId) -> + _setUserTCState userId, onForUser, true + applyClientTrackChangesStateToServer() + + ide.socket.on "toggle-track-changes", (state) -> $scope.$apply () -> - $scope.editor.wantTrackChanges = value + applyTrackChangesStateToClient(state) + + $scope.toggleTrackChangesFromKbdShortcut = () -> + if !$scope.project.features.trackChangesVisible + return + $scope.toggleTrackChangesForUser !$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, ide.$scope.user.id + + _inited = false + ide.$scope.$on "project:joined", () -> + return if _inited + project = ide.$scope.project + if project.features.trackChanges + window.trackChangesState ?= false + applyTrackChangesStateToClient(window.trackChangesState) + else + applyTrackChangesStateToClient(false) + _inited = true _refreshingRangeUsers = false _refreshedForUserIds = {} @@ -676,3 +755,11 @@ define [ controller: "TrackChangesUpgradeModalController" scope: $scope.$new() } + + $scope.openPerUserTCNoticeModal = () -> + $scope.reviewPanel.showPerUserTCNotice = false + $modal.open({ + templateUrl: "perUserTCNoticeModalTemplate" + }).result.finally () -> + event_tracking.sendMB "shown-per-user-tc-notice" + diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee index 2b5180dce6..6f2288634b 100644 --- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee @@ -4,20 +4,23 @@ define [ App.directive "reviewPanelToggle", () -> restrict: "E" scope: - onToggle: '=' + onToggle: '&' ngModel: '=' + valWhenUndefined: '=?' disabled: '=?' - onDisabledClick: '=?' + onDisabledClick: '&?' link: (scope) -> if !scope.disabled? scope.disabled = false scope.onChange = (args...) -> - scope.onToggle(scope.localModel) + scope.onToggle({ isOn: scope.localModel }) scope.handleClick = () -> - if scope.disabled + if scope.disabled and scope.onDisabledClick? scope.onDisabledClick() scope.localModel = scope.ngModel scope.$watch "ngModel", (value) -> + if scope.valWhenUndefined? and !value? + value = scope.valWhenUndefined scope.localModel = value template: """ diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 2f3a2c61e0..7bb86a6b93 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -30,7 +30,7 @@ define [ $scope.suggestions = suggestions $scope.contactUs = -> - if !$scope.form.email? + if !$scope.form.email? or $scope.form.email == "" console.log "email not set" return $scope.sending = true @@ -46,8 +46,16 @@ define [ about: "
browser: #{platform?.name} #{platform?.version}
os: #{platform?.os?.family} #{platform?.os?.version}
" - Groove.createTicket params, (err, json)-> - $scope.sent = true + Groove.createTicket params, (response)-> + $scope.sending = false + if response.responseText == "" # Blocked request or similar + $scope.error = true + else + data = JSON.parse(response.responseText) + if data.errors? + $scope.error = true + else + $scope.sent = true $scope.$apply() $scope.$watch "form.subject", (newVal, oldVal) -> diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 5d0341656e..cafbd7c81b 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -20,7 +20,7 @@ define [ , 10 $scope.$watch(( - () -> $scope.projects.filter((project) -> !project.tags? or project.tags.length == 0).length + () -> $scope.projects.filter((project) -> (!project.tags? or project.tags.length == 0) and !project.archived).length ), (newVal) -> $scope.nUntagged = newVal) storedUIOpts = JSON.parse(localStorage("project_list")) diff --git a/services/web/public/img/ol-brand/logo-horizontal.png b/services/web/public/img/ol-brand/logo-horizontal.png new file mode 100644 index 0000000000..6e95b282cb Binary files /dev/null and b/services/web/public/img/ol-brand/logo-horizontal.png differ diff --git a/services/web/public/img/ol-brand/overleaf-o-grey.svg b/services/web/public/img/ol-brand/overleaf-o-grey.svg new file mode 100644 index 0000000000..3b47c37cba --- /dev/null +++ b/services/web/public/img/ol-brand/overleaf-o-grey.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/services/web/public/img/ol-brand/overleaf-o.svg b/services/web/public/img/ol-brand/overleaf-o.svg new file mode 100644 index 0000000000..d95cee9ded --- /dev/null +++ b/services/web/public/img/ol-brand/overleaf-o.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/services/web/public/img/teasers/track-changes/per-user-track-changes.gif b/services/web/public/img/teasers/track-changes/per-user-track-changes.gif new file mode 100644 index 0000000000..ca7c636642 Binary files /dev/null and b/services/web/public/img/teasers/track-changes/per-user-track-changes.gif differ diff --git a/services/web/public/img/teasers/track-changes/per-user-track-changes.mp4 b/services/web/public/img/teasers/track-changes/per-user-track-changes.mp4 new file mode 100644 index 0000000000..9e4d0cbe29 Binary files /dev/null and b/services/web/public/img/teasers/track-changes/per-user-track-changes.mp4 differ 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 ede29b9661..df84725189 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 @@ -606,7 +606,7 @@ var createLatexWorker = function (session) { }; worker.on("lint", function(results) { if(docChangePending) { docChangePending = false; }; - hints = results.data; + hints = results.data.errors; if (hints.length > 100) { hints = hints.slice(0, 100); // limit to 100 errors }; 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 deleted file mode 100644 index d170f0ade5..0000000000 --- a/services/web/public/js/ace-1.2.5/mode-latex_beta.js +++ /dev/null @@ -1,655 +0,0 @@ -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/behaviour/latex",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"], function(require, exports, module) { -"use strict"; - -var oop = require("../../lib/oop"); -var Behaviour = require("../behaviour").Behaviour; -var TokenIterator = require("../../token_iterator").TokenIterator; -var lang = require("../../lib/lang"); - -var SAFE_INSERT_IN_TOKENS = - ["text", "paren.rparen", "punctuation.operator"]; -var SAFE_INSERT_BEFORE_TOKENS = - ["text", "paren.rparen", "punctuation.operator", "comment"]; - -var context; -var contextCache = {}; -var initContext = function(editor) { - var id = -1; - if (editor.multiSelect) { - id = editor.selection.index; - if (contextCache.rangeCount != editor.multiSelect.rangeCount) - contextCache = {rangeCount: editor.multiSelect.rangeCount}; - } - if (contextCache[id]) - return context = contextCache[id]; - context = contextCache[id] = { - autoInsertedBrackets: 0, - autoInsertedRow: -1, - autoInsertedLineEnd: "", - maybeInsertedBrackets: 0, - maybeInsertedRow: -1, - maybeInsertedLineStart: "", - maybeInsertedLineEnd: "" - }; -}; - -var getWrapped = function(selection, selected, opening, closing) { - var rowDiff = selection.end.row - selection.start.row; - return { - text: opening + selected + closing, - selection: [ - 0, - selection.start.column + 1, - rowDiff, - selection.end.column + (rowDiff ? 0 : 1) - ] - }; -}; - -var LatexBehaviour = function() { - this.add("braces", "insertion", function(state, action, editor, session, text) { - if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { - return; - } - var cursor = editor.getCursorPosition(); - var line = session.doc.getLine(cursor.row); - var lastChar = line[cursor.column-1]; - if (lastChar === '\\') { - return; - } - if (text == '{') { - initContext(editor); - var selection = editor.getSelectionRange(); - var selected = session.doc.getTextRange(selection); - if (selected !== "" && editor.getWrapBehavioursEnabled()) { - return getWrapped(selection, selected, '{', '}'); - } else if (LatexBehaviour.isSaneInsertion(editor, session)) { - LatexBehaviour.recordAutoInsert(editor, session, "}"); - return { - text: '{}', - selection: [1, 1] - }; - } - } else if (text == '}') { - initContext(editor); - var rightChar = line.substring(cursor.column, cursor.column + 1); - if (rightChar == '}') { - var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); - if (matching !== null && LatexBehaviour.isAutoInsertedClosing(cursor, line, text)) { - LatexBehaviour.popAutoInsertedClosing(); - return { - text: '', - selection: [1, 1] - }; - } - } - } - }); - - this.add("braces", "deletion", function(state, action, editor, session, range) { - if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { - return; - } - var selected = session.doc.getTextRange(range); - if (!range.isMultiLine() && selected == '{') { - initContext(editor); - var line = session.doc.getLine(range.start.row); - var rightChar = line.substring(range.start.column + 1, range.start.column + 2); - if (rightChar == '}') { - range.end.column++; - return range; - } - } - }); - - this.add("brackets", "insertion", function(state, action, editor, session, text) { - if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { - return; - } - var cursor = editor.getCursorPosition(); - var line = session.doc.getLine(cursor.row); - var lastChar = line[cursor.column-1]; - if (lastChar === '\\') { - return; - } - if (text == '[') { - initContext(editor); - var selection = editor.getSelectionRange(); - var selected = session.doc.getTextRange(selection); - if (selected !== "" && editor.getWrapBehavioursEnabled()) { - return getWrapped(selection, selected, '[', ']'); - } else if (LatexBehaviour.isSaneInsertion(editor, session)) { - LatexBehaviour.recordAutoInsert(editor, session, "]"); - return { - text: '[]', - selection: [1, 1] - }; - } - } else if (text == ']') { - initContext(editor); - var rightChar = line.substring(cursor.column, cursor.column + 1); - if (rightChar == ']') { - var matching = session.$findOpeningBracket(']', {column: cursor.column + 1, row: cursor.row}); - if (matching !== null && LatexBehaviour.isAutoInsertedClosing(cursor, line, text)) { - LatexBehaviour.popAutoInsertedClosing(); - return { - text: '', - selection: [1, 1] - }; - } - } - } - }); - - this.add("brackets", "deletion", function(state, action, editor, session, range) { - if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { - return; - } - var selected = session.doc.getTextRange(range); - if (!range.isMultiLine() && selected == '[') { - initContext(editor); - var line = session.doc.getLine(range.start.row); - var rightChar = line.substring(range.start.column + 1, range.start.column + 2); - if (rightChar == ']') { - range.end.column++; - return range; - } - } - }); - - this.add("dollars", "insertion", function(state, action, editor, session, text) { - var cursor = editor.getCursorPosition(); - var line = session.doc.getLine(cursor.row); - var lastChar = line[cursor.column-1]; - if (lastChar === '\\') { - return; - } - if (text == '$') { - if (this.lineCommentStart && this.lineCommentStart.indexOf(text) != -1) - return; - initContext(editor); - var quote = text; - var selection = editor.getSelectionRange(); - var selected = session.doc.getTextRange(selection); - if (selected !== "" && selected !== "$" && editor.getWrapBehavioursEnabled()) { - return getWrapped(selection, selected, quote, quote); - } else if (!selected) { - var leftChar = line.substring(cursor.column-1, cursor.column); - var rightChar = line.substring(cursor.column, cursor.column + 1); - - var token = session.getTokenAt(cursor.row, cursor.column); - var rightToken = session.getTokenAt(cursor.row, cursor.column + 1); - - var stringBefore = token && /string|escape/.test(token.type); - var stringAfter = !rightToken || /string|escape/.test(rightToken.type); - - var pair; - if (rightChar == quote) { - pair = stringBefore !== stringAfter; - if (pair && /string\.end/.test(rightToken.type)) - pair = false; - } else { - if (stringBefore && !stringAfter) - return null; // wrap string with different quote - if (stringBefore && stringAfter) - return null; // do not pair quotes inside strings - var wordRe = session.$mode.tokenRe; - wordRe.lastIndex = 0; - var isWordBefore = wordRe.test(leftChar); - wordRe.lastIndex = 0; - var isWordAfter = wordRe.test(leftChar); - if (isWordBefore || isWordAfter) - return null; // before or after alphanumeric - if (rightChar && !/[\s;,.})\]\\]/.test(rightChar)) - return null; // there is rightChar and it isn't closing - pair = true; - } - return { - text: pair ? quote + quote : "", - selection: [1,1] - }; - } - } - }); - - this.add("dollars", "deletion", function(state, action, editor, session, range) { - var selected = session.doc.getTextRange(range); - if (!range.isMultiLine() && (selected == '$')) { - initContext(editor); - var line = session.doc.getLine(range.start.row); - var rightChar = line.substring(range.start.column + 1, range.start.column + 2); - if (rightChar == selected) { - range.end.column++; - return range; - } - } - }); - -}; - - -LatexBehaviour.isSaneInsertion = function(editor, session) { - var cursor = editor.getCursorPosition(); - var iterator = new TokenIterator(session, cursor.row, cursor.column); - if (!this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) { - var iterator2 = new TokenIterator(session, cursor.row, cursor.column + 1); - if (!this.$matchTokenType(iterator2.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) - return false; - } - iterator.stepForward(); - return iterator.getCurrentTokenRow() !== cursor.row || - this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_BEFORE_TOKENS); -}; - -LatexBehaviour.$matchTokenType = function(token, types) { - return types.indexOf(token.type || token) > -1; -}; - -LatexBehaviour.recordAutoInsert = function(editor, session, bracket) { - var cursor = editor.getCursorPosition(); - var line = session.doc.getLine(cursor.row); - if (!this.isAutoInsertedClosing(cursor, line, context.autoInsertedLineEnd[0])) - context.autoInsertedBrackets = 0; - context.autoInsertedRow = cursor.row; - context.autoInsertedLineEnd = bracket + line.substr(cursor.column); - context.autoInsertedBrackets++; -}; - -LatexBehaviour.isAutoInsertedClosing = function(cursor, line, bracket) { - return context.autoInsertedBrackets > 0 && - cursor.row === context.autoInsertedRow && - bracket === context.autoInsertedLineEnd[0] && - line.substr(cursor.column) === context.autoInsertedLineEnd; -}; - -LatexBehaviour.popAutoInsertedClosing = function() { - context.autoInsertedLineEnd = context.autoInsertedLineEnd.substr(1); - context.autoInsertedBrackets--; -}; - - -oop.inherits(LatexBehaviour, Behaviour); - -exports.LatexBehaviour = LatexBehaviour; -}); - -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","ace/mode/behaviour/latex"], 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 LatexBehaviour = require("./behaviour/latex").LatexBehaviour; - -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.$behaviour = new LatexBehaviour(); - 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/worker-latex.js b/services/web/public/js/ace-1.2.5/worker-latex.js index ccd7e2da9b..dc9f5d3204 100644 --- a/services/web/public/js/ace-1.2.5/worker-latex.js +++ b/services/web/public/js/ace-1.2.5/worker-latex.js @@ -1419,6 +1419,34 @@ var LatexWorker = exports.LatexWorker = function(sender) { oop.inherits(LatexWorker, Mirror); +(function() { + var disabled = false; + this.onUpdate = function() { + if (disabled) { return ; }; + + var value = this.doc.getValue(); + var errors = []; + var contexts = []; + try { + if (value) { + var result = Parse(value); + errors = result.errors; + contexts = result.contexts; + } + } catch (e) { + console.log(e); + disabled = true; + this.sender.emit("fatal-error", e); + errors = []; + } + this.sender.emit("lint", { + errors: errors, + contexts: contexts + }); + }; + +}).call(LatexWorker.prototype); + var Tokenise = function (text) { var Tokens = []; var Comments = []; @@ -1503,22 +1531,8 @@ var Tokenise = function (text) { } 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 if (["{", "}", "$", "&", "#", "^", "_", "~"].indexOf(code) > -1) { // special characters + Tokens.push([lineNumber, code, pos, pos+1]); } else { throw "unrecognised character " + code; } @@ -1539,15 +1553,15 @@ var read1arg = function (TokeniseResult, k, options) { }; var open = Tokens[k+1]; - var env = Tokens[k+2]; + var delimiter = Tokens[k+2]; var close = Tokens[k+3]; - var envName; + var delimiterName; if(open && open[1] === "\\") { - envName = open[4]; // array element 4 is command sequence + delimiterName = 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 + } else if(open && open[1] === "{" && delimiter && delimiter[1] === "\\" && close && close[1] === "}") { + delimiterName = delimiter[4]; // NOTE: if we were actually using this, keep track of * above return k + 3; // array element 4 is command sequence } else { return null; @@ -1579,21 +1593,21 @@ var read1name = function (TokeniseResult, k) { var text = TokeniseResult.text; var open = Tokens[k+1]; - var env = Tokens[k+2]; + var delimiter = 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]); + if(open && open[1] === "{" && delimiter && delimiter[1] === "Text" && close && close[1] === "}") { + var delimiterName = text.substring(delimiter[2], delimiter[3]); return k + 3; - } else if (open && open[1] === "{" && env && env[1] === "Text") { - envName = ""; + } else if (open && open[1] === "{" && delimiter && delimiter[1] === "Text") { + delimiterName = ""; 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; + delimiterName = delimiterName + str; } else if (tok[1] === "_") { - envName = envName + "_"; + delimiterName = delimiterName + "_"; } else { break; } @@ -1644,6 +1658,7 @@ var readOptionalParams = function(TokeniseResult, k) { }; var count = 0; var nextToken = Tokens[k+1]; + if (!nextToken) { return null }; var pos = nextToken[2]; for (var i = pos, end = text.length; i < end; i++) { @@ -1788,7 +1803,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo; var TokenError = ErrorReporter.TokenError; - var Environments = new EnvHandler(ErrorReporter); + var Environments = new EnvHandler(TokeniseResult, ErrorReporter); var nextGroupMathMode = null; // if the next group should have var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes @@ -1815,28 +1830,28 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { if (type === "\\") { if (seq === "begin" || seq === "end") { var open = Tokens[i+1]; - var env = Tokens[i+2]; + var delimiter = 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}); + if(open && open[1] === "{" && delimiter && delimiter[1] === "Text" && close && close[1] === "}") { + var delimiterName = text.substring(delimiter[2], delimiter[3]); + Environments.push({command: seq, name: delimiterName, token: token, closeToken: close}); i = i + 3; // advance past these tokens } else { - if (open && open[1] === "{" && env && env[1] === "Text") { - envName = ""; + if (open && open[1] === "{" && delimiter && delimiter[1] === "Text") { + delimiterName = ""; 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; + delimiterName = delimiterName + str; } else if (tok[1] === "_") { - envName = envName + "_"; + delimiterName = delimiterName + "_"; } else { break; } } if (tok && tok[1] === "}") { - Environments.push({command: seq, name: envName, token: token, closeToken: close}); + Environments.push({command: seq, name: delimiterName, token: token, closeToken: close}); i = j; // advance past these tokens continue; } @@ -1844,8 +1859,8 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { 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 { + if (delimiter && delimiter[1] === "Text") { + endToken = delimiter.slice(); // we've got some text following the { start = endToken[2]; end = endToken[3]; for (j = start; j < end; j++) { var char = text[j]; @@ -1978,7 +1993,12 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { var nextIsDollar = lookAhead && lookAhead[1] === "$"; currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) { - Environments.push({command:"$$", token:token}); + if (currentMathMode && currentMathMode.command == "$$") { + var delimiterToken = lookAhead; + } else { + var delimiterToken = token; + } + Environments.push({command:"$$", token:delimiterToken}); i = i + 1; } else { Environments.push({command:"$", token:token}); @@ -1999,74 +2019,174 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) { return Environments; }; -var EnvHandler = function (ErrorReporter) { +var DocumentTree = function(TokeniseResult) { + var tree = { + children: [] + }; + var stack = [tree]; + + this.openEnv = function(startDelimiter) { + var currentNode = this.getCurrentNode(); + var newNode = { + startDelimiter: startDelimiter, + children: [] + }; + currentNode.children.push(newNode); + stack.push(newNode); + }; + + this.closeEnv = function(endDelimiter) { + if (stack.length == 1) { + return null + } + var currentNode = stack.pop(); + currentNode.endDelimiter = endDelimiter; + return currentNode.startDelimiter; + }; + + this.getNthPreviousNode = function(n) { + var offset = stack.length - n - 1; + if (offset < 0) + return null; + return stack[offset]; + } + + this.getCurrentNode = function() { + return this.getNthPreviousNode(0); + } + + this.getCurrentDelimiter = function() { + return this.getCurrentNode().startDelimiter; + }; + + this.getPreviousDelimiter = function() { + var node = this.getNthPreviousNode(1); + if (!node) + return null + return node.startDelimiter; + } + + this.getDepth = function() { + return (stack.length - 1) // Root node doesn't count + } + + this.getContexts = function() { + var linePosition = TokeniseResult.linePosition; + + function tokenToRange(token) { + var line = token[0], start = token[2], end = token[3]; + var start_col = start - linePosition[line]; + if (!end) { end = start + 1; } ; + var end_col = end - linePosition[line]; + return { + start: { + row: line, + column: start_col + }, + end: { + row: line, + column: end_col + } + } + }; + + function getContextsFromNode(node) { + if (node.startDelimiter && node.startDelimiter.mathMode) { + var context = { + type: "math", + range: { + start: tokenToRange(node.startDelimiter.token).start + } + }; + if (node.endDelimiter) { + var closeToken = node.endDelimiter.closeToken || node.endDelimiter.token; + context.range.end = tokenToRange(closeToken).end; + }; + return [context]; + } else { + var contexts = []; + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i]; + contexts = contexts.concat(getContextsFromNode(child)); + } + return contexts; + } + }; + + return getContextsFromNode(tree); + } +} + +var EnvHandler = function (TokeniseResult, ErrorReporter) { var ErrorTo = ErrorReporter.EnvErrorTo; var ErrorFromTo = ErrorReporter.EnvErrorFromTo; var ErrorFrom = ErrorReporter.EnvErrorFrom; - var envs = []; + var delimiters = []; - var state = []; + var document = new DocumentTree(TokeniseResult); 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.getDocument = function() { + return document; }; - this._endVerbatim = function (thisEnv) { - var lastEnv = state.pop(); - if (lastEnv && lastEnv.name === thisEnv.name) { + this.push = function (newDelimiter) { + this.setDelimiterProps(newDelimiter); + this.checkAndUpdateState(newDelimiter); + delimiters.push(newDelimiter); + }; + + this._endVerbatim = function (thisDelimiter) { + var lastDelimiter = document.getCurrentDelimiter(); + if (lastDelimiter && lastDelimiter.name === thisDelimiter.name) { inVerbatim = false; - verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]}); - } else { - if(lastEnv) { state.push(lastEnv); } ; + document.closeEnv(thisDelimiter); + verbatimRanges.push({start: lastDelimiter.token[2], end: thisDelimiter.token[2]}); } }; var invalidEnvs = []; - this._end = function (thisEnv) { + this._end = function (thisDelimiter) { do { - var lastEnv = state.pop(); + var lastDelimiter = document.getCurrentDelimiter(); var retry = false; var i; - if (closedBy(lastEnv, thisEnv)) { - if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) { - documentClosed = thisEnv; + if (closedBy(lastDelimiter, thisDelimiter)) { + document.closeEnv(thisDelimiter); + if (thisDelimiter.command === "end" && thisDelimiter.name === "document" && !documentClosed) { + documentClosed = thisDelimiter; }; return; - } else if (!lastEnv) { + } else if (!lastDelimiter) { if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); + ErrorFromTo(documentClosed, thisDelimiter, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); } else { - ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); + ErrorTo(thisDelimiter, "unexpected " + getName(thisDelimiter)); } - } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) { + } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisDelimiter) > -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); + var status = reportError(lastDelimiter, thisDelimiter); + if (delimiterPrecedence(lastDelimiter) < delimiterPrecedence(thisDelimiter)) { + document.closeEnv(); + invalidEnvs.push(lastDelimiter); retry = true; } else { - var prevLastEnv = state.pop(); - if(prevLastEnv) { - if (thisEnv.name === prevLastEnv.name) { + var prevDelimiter = document.getPreviousDelimiter(); + if(prevDelimiter) { + if (thisDelimiter.name === prevDelimiter.name) { + document.closeEnv() // Close current env + document.closeEnv(thisDelimiter) // Close previous env return; - } else { - state.push(prevLastEnv); } } - invalidEnvs.push(lastEnv); + invalidEnvs.push(lastDelimiter); } } @@ -2082,28 +2202,28 @@ var EnvHandler = function (ErrorReporter) { "$$": "$$" }; - var closedBy = function (lastEnv, thisEnv) { - if (!lastEnv) { + var closedBy = function (lastDelimiter, thisDelimiter) { + if (!lastDelimiter) { return false ; - } else if (thisEnv.command === "end") { - return lastEnv.command === "begin" && lastEnv.name === thisEnv.name; - } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) { + } else if (thisDelimiter.command === "end") { + return lastDelimiter.command === "begin" && lastDelimiter.name === thisDelimiter.name; + } else if (thisDelimiter.command === CLOSING_DELIMITER[lastDelimiter.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)) { + var indexOfClosingEnvInArray = function (delimiters, thisDelimiter) { + for (var i = 0, n = delimiters.length; i < n ; i++) { + if (closedBy(delimiters[i], thisDelimiter)) { return i; } } return -1; }; - var envPrecedence = function (env) { + var delimiterPrecedence = function (delimiter) { var openScore = { "{" : 1, "left" : 2, @@ -2118,14 +2238,14 @@ var EnvHandler = function (ErrorReporter) { "$$" : 5, "end": 4 }; - if (env.command) { - return openScore[env.command] || closeScore[env.command]; + if (delimiter.command) { + return openScore[delimiter.command] || closeScore[delimiter.command]; } else { return 0; } }; - var getName = function(env) { + var getName = function(delimiter) { var description = { "{" : "open group {", "}" : "close group }", @@ -2138,12 +2258,12 @@ var EnvHandler = function (ErrorReporter) { "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]; + if (delimiter.command === "begin" || delimiter.command === "end") { + return "\\" + delimiter.command + "{" + delimiter.name + "}"; + } else if (delimiter.command in description) { + return description[delimiter.command]; } else { - return env.command; + return delimiter.command; } }; @@ -2151,81 +2271,81 @@ var EnvHandler = function (ErrorReporter) { var UNCLOSED_GROUP = 2; var UNCLOSED_ENV = 3; - var reportError = function(lastEnv, thisEnv) { - if (!lastEnv) { // unexpected close, nothing was open! + var reportError = function(lastDelimiter, thisDelimiter) { + if (!lastDelimiter) { // unexpected close, nothing was open! if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); + ErrorFromTo(documentClosed, thisDelimiter, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); } else { - ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); + ErrorTo(thisDelimiter, "unexpected " + getName(thisDelimiter)); }; return EXTRA_CLOSE; - } else if (lastEnv.command === "{" && thisEnv.command === "end") { - ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), + } else if (lastDelimiter.command === "{" && thisDelimiter.command === "end") { + ErrorFromTo(lastDelimiter, thisDelimiter, "unclosed " + getName(lastDelimiter) + " found at " + getName(thisDelimiter), {suppressIfEditing:true, errorAtStart: true, type:"warning"}); return UNCLOSED_GROUP; } else { - var pLast = envPrecedence(lastEnv); - var pThis = envPrecedence(thisEnv); + var pLast = delimiterPrecedence(lastDelimiter); + var pThis = delimiterPrecedence(thisDelimiter); if (pThis > pLast) { - ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), + ErrorFromTo(lastDelimiter, thisDelimiter, "unclosed " + getName(lastDelimiter) + " found at " + getName(thisDelimiter), {suppressIfEditing:true, errorAtStart: true}); } else { - ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv)); + ErrorFromTo(lastDelimiter, thisDelimiter, "unexpected " + getName(thisDelimiter) + " after " + getName(lastDelimiter)); } return UNCLOSED_ENV; }; }; - this._beginMathMode = function (thisEnv) { + this._beginMathMode = function (thisDelimiter) { var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env if (currentMathMode) { - ErrorFrom(thisEnv, getName(thisEnv) + " used inside existing math mode " + getName(currentMathMode), + ErrorFrom(thisDelimiter, getName(thisDelimiter) + " used inside existing math mode " + getName(currentMathMode), {suppressIfEditing:true, errorAtStart: true, mathMode:true}); }; - thisEnv.mathMode = thisEnv; - state.push(thisEnv); + thisDelimiter.mathMode = thisDelimiter; + document.openEnv(thisDelimiter); }; - this._toggleMathMode = function (thisEnv) { - var lastEnv = state.pop(); - if (closedBy(lastEnv, thisEnv)) { + this._toggleMathMode = function (thisDelimiter) { + var lastDelimiter = document.getCurrentDelimiter(); + if (closedBy(lastDelimiter, thisDelimiter)) { + document.closeEnv(thisDelimiter) return; } else { - if (lastEnv) {state.push(lastEnv);} - if (lastEnv && lastEnv.mathMode) { - this._end(thisEnv); + if (lastDelimiter && lastDelimiter.mathMode) { + this._end(thisDelimiter); } else { - thisEnv.mathMode = thisEnv; - state.push(thisEnv); + thisDelimiter.mathMode = thisDelimiter; + document.openEnv(thisDelimiter); } }; }; this.getMathMode = function () { - var n = state.length; - if (n > 0) { - return state[n-1].mathMode; + var currentDelimiter = document.getCurrentDelimiter(); + if (currentDelimiter) { + return currentDelimiter.mathMode; } else { return null; } }; this.insideGroup = function () { - var n = state.length; - if (n > 0) { - return (state[n-1].command === "{"); + var currentDelimiter = document.getCurrentDelimiter(); + if (currentDelimiter) { + return (currentDelimiter.command === "{"); } else { return null; } }; var resetMathMode = function () { - var n = state.length; - if (n > 0) { - var lastMathMode = state[n-1].mathMode; + var currentDelimiter = document.getCurrentDelimiter(); + if (currentDelimiter) { + var lastMathMode = currentDelimiter.mathMode; do { - var lastEnv = state.pop(); - } while (lastEnv && lastEnv !== lastMathMode); + var lastDelimiter = document.closeEnv(); + } while (lastDelimiter && lastDelimiter !== lastMathMode); } else { return; } @@ -2233,42 +2353,42 @@ var EnvHandler = function (ErrorReporter) { this.resetMathMode = resetMathMode; - var getNewMathMode = function (currentMathMode, thisEnv) { + var getNewMathMode = function (currentMathMode, thisDelimiter) { var newMathMode = null; - if (thisEnv.command === "{") { - if (thisEnv.mathMode !== null) { - newMathMode = thisEnv.mathMode; + if (thisDelimiter.command === "{") { + if (thisDelimiter.mathMode !== null) { + newMathMode = thisDelimiter.mathMode; } else { newMathMode = currentMathMode; } - } else if (thisEnv.command === "left") { + } else if (thisDelimiter.command === "left") { if (currentMathMode === null) { - ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true}); + ErrorFrom(thisDelimiter, "\\left can only be used in math mode", {mathMode: true}); }; newMathMode = currentMathMode; - } else if (thisEnv.command === "begin") { - var name = thisEnv.name; + } else if (thisDelimiter.command === "begin") { + var name = thisDelimiter.name; if (name) { if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { if (currentMathMode) { - ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), + ErrorFromTo(currentMathMode, thisDelimiter, thisDelimiter.name + " used inside " + getName(currentMathMode), {suppressIfEditing:true, errorAtStart: true, mathMode: true}); resetMathMode(); }; newMathMode = null; } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) { if (currentMathMode === null) { - ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true}); + ErrorFrom(thisDelimiter, thisDelimiter.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), + ErrorFromTo(currentMathMode, thisDelimiter, thisDelimiter.name + " used inside " + getName(currentMathMode), {suppressIfEditing:true, errorAtStart: true, mathMode: true}); resetMathMode(); }; - newMathMode = thisEnv; + newMathMode = thisDelimiter; } else { newMathMode = undefined; // undefined means we don't know if we are in math mode or not } @@ -2277,41 +2397,41 @@ var EnvHandler = function (ErrorReporter) { return newMathMode; }; - this.checkAndUpdateState = function (thisEnv) { + this.checkAndUpdateState = function (thisDelimiter) { if (inVerbatim) { - if (thisEnv.command === "end") { - this._endVerbatim(thisEnv); + if (thisDelimiter.command === "end") { + this._endVerbatim(thisDelimiter); } else { return; // ignore anything in verbatim environments } - } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") { - if (thisEnv.verbatim) {inVerbatim = true;}; + } else if(thisDelimiter.command === "begin" || thisDelimiter.command === "{" || thisDelimiter.command === "left") { + if (thisDelimiter.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); + var newMathMode = getNewMathMode(currentMathMode, thisDelimiter); + thisDelimiter.mathMode = newMathMode; + document.openEnv(thisDelimiter); + } else if (thisDelimiter.command === "end") { + this._end(thisDelimiter); + } else if (thisDelimiter.command === "(" || thisDelimiter.command === "[") { + this._beginMathMode(thisDelimiter); + } else if (thisDelimiter.command === ")" || thisDelimiter.command === "]") { + this._end(thisDelimiter); + } else if (thisDelimiter.command === "}") { + this._end(thisDelimiter); + } else if (thisDelimiter.command === "right") { + this._end(thisDelimiter); + } else if (thisDelimiter.command === "$" || thisDelimiter.command === "$$") { + this._toggleMathMode(thisDelimiter); } }; this.close = function () { - while (state.length > 0) { - var thisEnv = state.pop(); - if (thisEnv.command === "{") { - ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); + while (document.getDepth() > 0) { + var thisDelimiter = document.closeEnv(); + if (thisDelimiter.command === "{") { + ErrorFrom(thisDelimiter, "unclosed group {", {type:"warning"}); } else { - ErrorFrom(thisEnv, "unclosed " + getName(thisEnv)); + ErrorFrom(thisDelimiter, "unclosed " + getName(thisDelimiter)); } } var vlen = verbatimRanges.length; @@ -2331,10 +2451,10 @@ var EnvHandler = function (ErrorReporter) { } }; - this.setEnvProps = function (env) { - var name = env.name ; + this.setDelimiterProps = function (delimiter) { + var name = delimiter.name ; if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) { - env.verbatim = true; + delimiter.verbatim = true; } }; }; @@ -2458,9 +2578,9 @@ var ErrorReporter = function (TokeniseResult) { errors.push(err); }; - this.EnvErrorFrom = function (env, message, options) { + this.EnvErrorFrom = function (delimiter, message, options) { if(!options) { options = {} ; }; - var token = env.token; + var token = delimiter.token; var line = token[0], type = token[1], start = token[2], end = token[3]; var start_col = start - linePosition[line]; var end_col = Infinity; @@ -2481,29 +2601,11 @@ var Parse = function (text) { var Reporter = new ErrorReporter(TokeniseResult); var Environments = InterpretTokens(TokeniseResult, Reporter); Environments.close(); - return Reporter.getErrors(); + return { + errors: Reporter.getErrors(), + contexts: Environments.getDocument().getContexts() + } }; - -(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) { 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 deleted file mode 100644 index 720c3e5009..0000000000 --- a/services/web/public/js/ace-1.2.5/worker-latex_beta.js +++ /dev/null @@ -1,3207 +0,0 @@ -"no use strict"; -;(function(window) { -if (typeof window.window != "undefined" && window.document) - return; -if (window.require && window.define) - return; - -if (!window.console) { - window.console = function() { - var msgs = Array.prototype.slice.call(arguments, 0); - postMessage({type: "log", data: msgs}); - }; - window.console.error = - window.console.warn = - window.console.log = - window.console.trace = window.console; -} -window.window = window; -window.ace = window; - -window.onerror = function(message, file, line, col, err) { - postMessage({type: "error", data: { - message: message, - data: err.data, - file: file, - line: line, - col: col, - stack: err.stack - }}); -}; - -window.normalizeModule = function(parentId, moduleName) { - // normalize plugin requires - if (moduleName.indexOf("!") !== -1) { - var chunks = moduleName.split("!"); - return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]); - } - // normalize relative requires - if (moduleName.charAt(0) == ".") { - var base = parentId.split("/").slice(0, -1).join("/"); - moduleName = (base ? base + "/" : "") + moduleName; - - while (moduleName.indexOf(".") !== -1 && previous != moduleName) { - var previous = moduleName; - moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); - } - } - - return moduleName; -}; - -window.require = function require(parentId, id) { - if (!id) { - id = parentId; - parentId = null; - } - if (!id.charAt) - throw new Error("worker.js require() accepts only (parentId, id) as arguments"); - - id = window.normalizeModule(parentId, id); - - var module = window.require.modules[id]; - if (module) { - if (!module.initialized) { - module.initialized = true; - module.exports = module.factory().exports; - } - return module.exports; - } - - if (!window.require.tlns) - return console.log("unable to load " + id); - - var path = resolveModuleId(id, window.require.tlns); - if (path.slice(-3) != ".js") path += ".js"; - - window.require.id = id; - window.require.modules[id] = {}; // prevent infinite loop on broken modules - importScripts(path); - return window.require(parentId, id); -}; -function resolveModuleId(id, paths) { - var testPath = id, tail = ""; - while (testPath) { - var alias = paths[testPath]; - if (typeof alias == "string") { - return alias + tail; - } else if (alias) { - return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name); - } else if (alias === false) { - return ""; - } - var i = testPath.lastIndexOf("/"); - if (i === -1) break; - tail = testPath.substr(i) + tail; - testPath = testPath.slice(0, i); - } - return id; -} -window.require.modules = {}; -window.require.tlns = {}; - -window.define = function(id, deps, factory) { - if (arguments.length == 2) { - factory = deps; - if (typeof id != "string") { - deps = id; - id = window.require.id; - } - } else if (arguments.length == 1) { - factory = id; - deps = []; - id = window.require.id; - } - - if (typeof factory != "function") { - window.require.modules[id] = { - exports: factory, - initialized: true - }; - return; - } - - if (!deps.length) - // If there is no dependencies, we inject "require", "exports" and - // "module" as dependencies, to provide CommonJS compatibility. - deps = ["require", "exports", "module"]; - - var req = function(childId) { - return window.require(id, childId); - }; - - window.require.modules[id] = { - exports: {}, - factory: function() { - var module = this; - var returnExports = factory.apply(this, deps.map(function(dep) { - switch (dep) { - // Because "require", "exports" and "module" aren't actual - // dependencies, we must handle them seperately. - case "require": return req; - case "exports": return module.exports; - case "module": return module; - // But for all other dependencies, we can just go ahead and - // require them. - default: return req(dep); - } - })); - if (returnExports) - module.exports = returnExports; - return module; - } - }; -}; -window.define.amd = {}; -require.tlns = {}; -window.initBaseUrls = function initBaseUrls(topLevelNamespaces) { - for (var i in topLevelNamespaces) - require.tlns[i] = topLevelNamespaces[i]; -}; - -window.initSender = function initSender() { - - var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; - var oop = window.require("ace/lib/oop"); - - var Sender = function() {}; - - (function() { - - oop.implement(this, EventEmitter); - - this.callback = function(data, callbackId) { - postMessage({ - type: "call", - id: callbackId, - data: data - }); - }; - - this.emit = function(name, data) { - postMessage({ - type: "event", - name: name, - data: data - }); - }; - - }).call(Sender.prototype); - - return new Sender(); -}; - -var main = window.main = null; -var sender = window.sender = null; - -window.onmessage = function(e) { - var msg = e.data; - if (msg.event && sender) { - sender._signal(msg.event, msg.data); - } - else if (msg.command) { - if (main[msg.command]) - main[msg.command].apply(main, msg.args); - else if (window[msg.command]) - window[msg.command].apply(window, msg.args); - else - throw new Error("Unknown command:" + msg.command); - } - else if (msg.init) { - window.initBaseUrls(msg.tlns); - require("ace/lib/es5-shim"); - sender = window.sender = window.initSender(); - var clazz = require(msg.module)[msg.classname]; - main = window.main = new clazz(sender); - } -}; -})(this); - -ace.define("ace/lib/oop",["require","exports","module"], function(require, exports, module) { -"use strict"; - -exports.inherits = function(ctor, superCtor) { - ctor.super_ = superCtor; - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); -}; - -exports.mixin = function(obj, mixin) { - for (var key in mixin) { - obj[key] = mixin[key]; - } - return obj; -}; - -exports.implement = function(proto, mixin) { - exports.mixin(proto, mixin); -}; - -}); - -ace.define("ace/range",["require","exports","module"], function(require, exports, module) { -"use strict"; -var comparePoints = function(p1, p2) { - return p1.row - p2.row || p1.column - p2.column; -}; -var Range = function(startRow, startColumn, endRow, endColumn) { - this.start = { - row: startRow, - column: startColumn - }; - - this.end = { - row: endRow, - column: endColumn - }; -}; - -(function() { - this.isEqual = function(range) { - return this.start.row === range.start.row && - this.end.row === range.end.row && - this.start.column === range.start.column && - this.end.column === range.end.column; - }; - this.toString = function() { - return ("Range: [" + this.start.row + "/" + this.start.column + - "] -> [" + this.end.row + "/" + this.end.column + "]"); - }; - - this.contains = function(row, column) { - return this.compare(row, column) == 0; - }; - this.compareRange = function(range) { - var cmp, - end = range.end, - start = range.start; - - cmp = this.compare(end.row, end.column); - if (cmp == 1) { - cmp = this.compare(start.row, start.column); - if (cmp == 1) { - return 2; - } else if (cmp == 0) { - return 1; - } else { - return 0; - } - } else if (cmp == -1) { - return -2; - } else { - cmp = this.compare(start.row, start.column); - if (cmp == -1) { - return -1; - } else if (cmp == 1) { - return 42; - } else { - return 0; - } - } - }; - this.comparePoint = function(p) { - return this.compare(p.row, p.column); - }; - this.containsRange = function(range) { - return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; - }; - this.intersects = function(range) { - var cmp = this.compareRange(range); - return (cmp == -1 || cmp == 0 || cmp == 1); - }; - this.isEnd = function(row, column) { - return this.end.row == row && this.end.column == column; - }; - this.isStart = function(row, column) { - return this.start.row == row && this.start.column == column; - }; - this.setStart = function(row, column) { - if (typeof row == "object") { - this.start.column = row.column; - this.start.row = row.row; - } else { - this.start.row = row; - this.start.column = column; - } - }; - this.setEnd = function(row, column) { - if (typeof row == "object") { - this.end.column = row.column; - this.end.row = row.row; - } else { - this.end.row = row; - this.end.column = column; - } - }; - this.inside = function(row, column) { - if (this.compare(row, column) == 0) { - if (this.isEnd(row, column) || this.isStart(row, column)) { - return false; - } else { - return true; - } - } - return false; - }; - this.insideStart = function(row, column) { - if (this.compare(row, column) == 0) { - if (this.isEnd(row, column)) { - return false; - } else { - return true; - } - } - return false; - }; - this.insideEnd = function(row, column) { - if (this.compare(row, column) == 0) { - if (this.isStart(row, column)) { - return false; - } else { - return true; - } - } - return false; - }; - this.compare = function(row, column) { - if (!this.isMultiLine()) { - if (row === this.start.row) { - return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); - } - } - - if (row < this.start.row) - return -1; - - if (row > this.end.row) - return 1; - - if (this.start.row === row) - return column >= this.start.column ? 0 : -1; - - if (this.end.row === row) - return column <= this.end.column ? 0 : 1; - - return 0; - }; - this.compareStart = function(row, column) { - if (this.start.row == row && this.start.column == column) { - return -1; - } else { - return this.compare(row, column); - } - }; - this.compareEnd = function(row, column) { - if (this.end.row == row && this.end.column == column) { - return 1; - } else { - return this.compare(row, column); - } - }; - this.compareInside = function(row, column) { - if (this.end.row == row && this.end.column == column) { - return 1; - } else if (this.start.row == row && this.start.column == column) { - return -1; - } else { - return this.compare(row, column); - } - }; - this.clipRows = function(firstRow, lastRow) { - if (this.end.row > lastRow) - var end = {row: lastRow + 1, column: 0}; - else if (this.end.row < firstRow) - var end = {row: firstRow, column: 0}; - - if (this.start.row > lastRow) - var start = {row: lastRow + 1, column: 0}; - else if (this.start.row < firstRow) - var start = {row: firstRow, column: 0}; - - return Range.fromPoints(start || this.start, end || this.end); - }; - this.extend = function(row, column) { - var cmp = this.compare(row, column); - - if (cmp == 0) - return this; - else if (cmp == -1) - var start = {row: row, column: column}; - else - var end = {row: row, column: column}; - - return Range.fromPoints(start || this.start, end || this.end); - }; - - this.isEmpty = function() { - return (this.start.row === this.end.row && this.start.column === this.end.column); - }; - this.isMultiLine = function() { - return (this.start.row !== this.end.row); - }; - this.clone = function() { - return Range.fromPoints(this.start, this.end); - }; - this.collapseRows = function() { - if (this.end.column == 0) - return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) - else - return new Range(this.start.row, 0, this.end.row, 0) - }; - this.toScreenRange = function(session) { - var screenPosStart = session.documentToScreenPosition(this.start); - var screenPosEnd = session.documentToScreenPosition(this.end); - - return new Range( - screenPosStart.row, screenPosStart.column, - screenPosEnd.row, screenPosEnd.column - ); - }; - this.moveBy = function(row, column) { - this.start.row += row; - this.start.column += column; - this.end.row += row; - this.end.column += column; - }; - -}).call(Range.prototype); -Range.fromPoints = function(start, end) { - return new Range(start.row, start.column, end.row, end.column); -}; -Range.comparePoints = comparePoints; - -Range.comparePoints = function(p1, p2) { - return p1.row - p2.row || p1.column - p2.column; -}; - - -exports.Range = Range; -}); - -ace.define("ace/apply_delta",["require","exports","module"], function(require, exports, module) { -"use strict"; - -function throwDeltaError(delta, errorText){ - console.log("Invalid Delta:", delta); - throw "Invalid Delta: " + errorText; -} - -function positionInDocument(docLines, position) { - return position.row >= 0 && position.row < docLines.length && - position.column >= 0 && position.column <= docLines[position.row].length; -} - -function validateDelta(docLines, delta) { - if (delta.action != "insert" && delta.action != "remove") - throwDeltaError(delta, "delta.action must be 'insert' or 'remove'"); - if (!(delta.lines instanceof Array)) - throwDeltaError(delta, "delta.lines must be an Array"); - if (!delta.start || !delta.end) - throwDeltaError(delta, "delta.start/end must be an present"); - var start = delta.start; - if (!positionInDocument(docLines, delta.start)) - throwDeltaError(delta, "delta.start must be contained in document"); - var end = delta.end; - if (delta.action == "remove" && !positionInDocument(docLines, end)) - throwDeltaError(delta, "delta.end must contained in document for 'remove' actions"); - var numRangeRows = end.row - start.row; - var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); - if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) - throwDeltaError(delta, "delta.range must match delta lines"); -} - -exports.applyDelta = function(docLines, delta, doNotValidate) { - - var row = delta.start.row; - var startColumn = delta.start.column; - var line = docLines[row] || ""; - switch (delta.action) { - case "insert": - var lines = delta.lines; - if (lines.length === 1) { - docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); - } else { - var args = [row, 1].concat(delta.lines); - docLines.splice.apply(docLines, args); - docLines[row] = line.substring(0, startColumn) + docLines[row]; - docLines[row + delta.lines.length - 1] += line.substring(startColumn); - } - break; - case "remove": - var endColumn = delta.end.column; - var endRow = delta.end.row; - if (row === endRow) { - docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); - } else { - docLines.splice( - row, endRow - row + 1, - line.substring(0, startColumn) + docLines[endRow].substring(endColumn) - ); - } - break; - } -} -}); - -ace.define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) { -"use strict"; - -var EventEmitter = {}; -var stopPropagation = function() { this.propagationStopped = true; }; -var preventDefault = function() { this.defaultPrevented = true; }; - -EventEmitter._emit = -EventEmitter._dispatchEvent = function(eventName, e) { - this._eventRegistry || (this._eventRegistry = {}); - this._defaultHandlers || (this._defaultHandlers = {}); - - var listeners = this._eventRegistry[eventName] || []; - var defaultHandler = this._defaultHandlers[eventName]; - if (!listeners.length && !defaultHandler) - return; - - if (typeof e != "object" || !e) - e = {}; - - if (!e.type) - e.type = eventName; - if (!e.stopPropagation) - e.stopPropagation = stopPropagation; - if (!e.preventDefault) - e.preventDefault = preventDefault; - - listeners = listeners.slice(); - for (var i=0; i this.row) - return; - - var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); - this.setPosition(point.row, point.column, true); - }; - - function $pointsInOrder(point1, point2, equalPointsInOrder) { - var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; - return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); - } - - function $getTransformedPoint(delta, point, moveIfEqual) { - var deltaIsInsert = delta.action == "insert"; - var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); - var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column); - var deltaStart = delta.start; - var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range. - if ($pointsInOrder(point, deltaStart, moveIfEqual)) { - return { - row: point.row, - column: point.column - }; - } - if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { - return { - row: point.row + deltaRowShift, - column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) - }; - } - - return { - row: deltaStart.row, - column: deltaStart.column - }; - } - this.setPosition = function(row, column, noClip) { - var pos; - if (noClip) { - pos = { - row: row, - column: column - }; - } else { - pos = this.$clipPositionToDocument(row, column); - } - - if (this.row == pos.row && this.column == pos.column) - return; - - var old = { - row: this.row, - column: this.column - }; - - this.row = pos.row; - this.column = pos.column; - this._signal("change", { - old: old, - value: pos - }); - }; - this.detach = function() { - this.document.removeEventListener("change", this.$onChange); - }; - this.attach = function(doc) { - this.document = doc || this.document; - this.document.on("change", this.$onChange); - }; - this.$clipPositionToDocument = function(row, column) { - var pos = {}; - - if (row >= this.document.getLength()) { - pos.row = Math.max(0, this.document.getLength() - 1); - pos.column = this.document.getLine(pos.row).length; - } - else if (row < 0) { - pos.row = 0; - pos.column = 0; - } - else { - pos.row = row; - pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); - } - - if (column < 0) - pos.column = 0; - - return pos; - }; - -}).call(Anchor.prototype); - -}); - -ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) { -"use strict"; - -var oop = require("./lib/oop"); -var applyDelta = require("./apply_delta").applyDelta; -var EventEmitter = require("./lib/event_emitter").EventEmitter; -var Range = require("./range").Range; -var Anchor = require("./anchor").Anchor; - -var Document = function(textOrLines) { - this.$lines = [""]; - if (textOrLines.length === 0) { - this.$lines = [""]; - } else if (Array.isArray(textOrLines)) { - this.insertMergedLines({row: 0, column: 0}, textOrLines); - } else { - this.insert({row: 0, column:0}, textOrLines); - } -}; - -(function() { - - oop.implement(this, EventEmitter); - this.setValue = function(text) { - var len = this.getLength() - 1; - this.remove(new Range(0, 0, len, this.getLine(len).length)); - this.insert({row: 0, column: 0}, text); - }; - this.getValue = function() { - return this.getAllLines().join(this.getNewLineCharacter()); - }; - this.createAnchor = function(row, column) { - return new Anchor(this, row, column); - }; - if ("aaa".split(/a/).length === 0) { - this.$split = function(text) { - return text.replace(/\r\n|\r/g, "\n").split("\n"); - }; - } else { - this.$split = function(text) { - return text.split(/\r\n|\r|\n/); - }; - } - - - this.$detectNewLine = function(text) { - var match = text.match(/^.*?(\r\n|\r|\n)/m); - this.$autoNewLine = match ? match[1] : "\n"; - this._signal("changeNewLineMode"); - }; - this.getNewLineCharacter = function() { - switch (this.$newLineMode) { - case "windows": - return "\r\n"; - case "unix": - return "\n"; - default: - return this.$autoNewLine || "\n"; - } - }; - - this.$autoNewLine = ""; - this.$newLineMode = "auto"; - this.setNewLineMode = function(newLineMode) { - if (this.$newLineMode === newLineMode) - return; - - this.$newLineMode = newLineMode; - this._signal("changeNewLineMode"); - }; - this.getNewLineMode = function() { - return this.$newLineMode; - }; - this.isNewLine = function(text) { - return (text == "\r\n" || text == "\r" || text == "\n"); - }; - this.getLine = function(row) { - return this.$lines[row] || ""; - }; - this.getLines = function(firstRow, lastRow) { - return this.$lines.slice(firstRow, lastRow + 1); - }; - this.getAllLines = function() { - return this.getLines(0, this.getLength()); - }; - this.getLength = function() { - return this.$lines.length; - }; - this.getTextRange = function(range) { - return this.getLinesForRange(range).join(this.getNewLineCharacter()); - }; - this.getLinesForRange = function(range) { - var lines; - if (range.start.row === range.end.row) { - lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; - } else { - lines = this.getLines(range.start.row, range.end.row); - lines[0] = (lines[0] || "").substring(range.start.column); - var l = lines.length - 1; - if (range.end.row - range.start.row == l) - lines[l] = lines[l].substring(0, range.end.column); - } - return lines; - }; - this.insertLines = function(row, lines) { - console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."); - return this.insertFullLines(row, lines); - }; - this.removeLines = function(firstRow, lastRow) { - console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."); - return this.removeFullLines(firstRow, lastRow); - }; - this.insertNewLine = function(position) { - console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."); - return this.insertMergedLines(position, ["", ""]); - }; - this.insert = function(position, text) { - if (this.getLength() <= 1) - this.$detectNewLine(text); - - return this.insertMergedLines(position, this.$split(text)); - }; - this.insertInLine = function(position, text) { - var start = this.clippedPos(position.row, position.column); - var end = this.pos(position.row, position.column + text.length); - - this.applyDelta({ - start: start, - end: end, - action: "insert", - lines: [text] - }, true); - - return this.clonePos(end); - }; - - this.clippedPos = function(row, column) { - var length = this.getLength(); - if (row === undefined) { - row = length; - } else if (row < 0) { - row = 0; - } else if (row >= length) { - row = length - 1; - column = undefined; - } - var line = this.getLine(row); - if (column == undefined) - column = line.length; - column = Math.min(Math.max(column, 0), line.length); - return {row: row, column: column}; - }; - - this.clonePos = function(pos) { - return {row: pos.row, column: pos.column}; - }; - - this.pos = function(row, column) { - return {row: row, column: column}; - }; - - this.$clipPosition = function(position) { - var length = this.getLength(); - if (position.row >= length) { - position.row = Math.max(0, length - 1); - position.column = this.getLine(length - 1).length; - } else { - position.row = Math.max(0, position.row); - position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); - } - return position; - }; - this.insertFullLines = function(row, lines) { - row = Math.min(Math.max(row, 0), this.getLength()); - var column = 0; - if (row < this.getLength()) { - lines = lines.concat([""]); - column = 0; - } else { - lines = [""].concat(lines); - row--; - column = this.$lines[row].length; - } - this.insertMergedLines({row: row, column: column}, lines); - }; - this.insertMergedLines = function(position, lines) { - var start = this.clippedPos(position.row, position.column); - var end = { - row: start.row + lines.length - 1, - column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length - }; - - this.applyDelta({ - start: start, - end: end, - action: "insert", - lines: lines - }); - - return this.clonePos(end); - }; - this.remove = function(range) { - var start = this.clippedPos(range.start.row, range.start.column); - var end = this.clippedPos(range.end.row, range.end.column); - this.applyDelta({ - start: start, - end: end, - action: "remove", - lines: this.getLinesForRange({start: start, end: end}) - }); - return this.clonePos(start); - }; - this.removeInLine = function(row, startColumn, endColumn) { - var start = this.clippedPos(row, startColumn); - var end = this.clippedPos(row, endColumn); - - this.applyDelta({ - start: start, - end: end, - action: "remove", - lines: this.getLinesForRange({start: start, end: end}) - }, true); - - return this.clonePos(start); - }; - this.removeFullLines = function(firstRow, lastRow) { - firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); - lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); - var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; - var deleteLastNewLine = lastRow < this.getLength() - 1; - var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); - var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); - var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); - var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); - var range = new Range(startRow, startCol, endRow, endCol); - var deletedLines = this.$lines.slice(firstRow, lastRow + 1); - - this.applyDelta({ - start: range.start, - end: range.end, - action: "remove", - lines: this.getLinesForRange(range) - }); - return deletedLines; - }; - this.removeNewLine = function(row) { - if (row < this.getLength() - 1 && row >= 0) { - this.applyDelta({ - start: this.pos(row, this.getLine(row).length), - end: this.pos(row + 1, 0), - action: "remove", - lines: ["", ""] - }); - } - }; - this.replace = function(range, text) { - if (!(range instanceof Range)) - range = Range.fromPoints(range.start, range.end); - if (text.length === 0 && range.isEmpty()) - return range.start; - if (text == this.getTextRange(range)) - return range.end; - - this.remove(range); - var end; - if (text) { - end = this.insert(range.start, text); - } - else { - end = range.start; - } - - return end; - }; - this.applyDeltas = function(deltas) { - for (var i=0; i=0; i--) { - this.revertDelta(deltas[i]); - } - }; - this.applyDelta = function(delta, doNotValidate) { - var isInsert = delta.action == "insert"; - if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] - : !Range.comparePoints(delta.start, delta.end)) { - return; - } - - if (isInsert && delta.lines.length > 20000) - this.$splitAndapplyLargeDelta(delta, 20000); - applyDelta(this.$lines, delta, doNotValidate); - this._signal("change", delta); - }; - - this.$splitAndapplyLargeDelta = function(delta, MAX) { - var lines = delta.lines; - var l = lines.length; - var row = delta.start.row; - var column = delta.start.column; - var from = 0, to = 0; - do { - from = to; - to += MAX - 1; - var chunk = lines.slice(from, to); - if (to > l) { - delta.lines = chunk; - delta.start.row = row + from; - delta.start.column = column; - break; - } - chunk.push(""); - this.applyDelta({ - start: this.pos(row + from, column), - end: this.pos(row + to, column = 0), - action: delta.action, - lines: chunk - }, true); - } while(true); - }; - this.revertDelta = function(delta) { - this.applyDelta({ - start: this.clonePos(delta.start), - end: this.clonePos(delta.end), - action: (delta.action == "insert" ? "remove" : "insert"), - lines: delta.lines.slice() - }); - }; - this.indexToPosition = function(index, startRow) { - var lines = this.$lines || this.getAllLines(); - var newlineLength = this.getNewLineCharacter().length; - for (var i = startRow || 0, l = lines.length; i < l; i++) { - index -= lines[i].length + newlineLength; - if (index < 0) - return {row: i, column: index + lines[i].length + newlineLength}; - } - return {row: l-1, column: lines[l-1].length}; - }; - this.positionToIndex = function(pos, startRow) { - var lines = this.$lines || this.getAllLines(); - var newlineLength = this.getNewLineCharacter().length; - var index = 0; - var row = Math.min(pos.row, lines.length); - for (var i = startRow || 0; i < row; ++i) - index += lines[i].length + newlineLength; - - return index + pos.column; - }; - -}).call(Document.prototype); - -exports.Document = Document; -}); - -ace.define("ace/lib/lang",["require","exports","module"], function(require, exports, module) { -"use strict"; - -exports.last = function(a) { - return a[a.length - 1]; -}; - -exports.stringReverse = function(string) { - return string.split("").reverse().join(""); -}; - -exports.stringRepeat = function (string, count) { - var result = ''; - while (count > 0) { - if (count & 1) - result += string; - - if (count >>= 1) - string += string; - } - return result; -}; - -var trimBeginRegexp = /^\s\s*/; -var trimEndRegexp = /\s\s*$/; - -exports.stringTrimLeft = function (string) { - return string.replace(trimBeginRegexp, ''); -}; - -exports.stringTrimRight = function (string) { - return string.replace(trimEndRegexp, ''); -}; - -exports.copyObject = function(obj) { - var copy = {}; - for (var key in obj) { - copy[key] = obj[key]; - } - return copy; -}; - -exports.copyArray = function(array){ - var copy = []; - for (var i=0, l=array.length; i MAX_TOKENS) { - throw new Error("exceed max token count of " + MAX_TOKENS); - break; - }; - var result = SPECIAL.exec(text); - if (result == null) { - if (idx < text.length) { - Tokens.push([lineNumber, "Text", idx, text.length]); - } - break; - } - if (result && result.index <= pos) { - throw new Error("infinite loop in parsing"); - break; - }; - pos = result.index; - if (pos > idx) { - Tokens.push([lineNumber, "Text", idx, pos]); - } - for (var i = idx; i < pos; i++) { - if (text[i] === "\n") { - lineNumber++; - linePosition[lineNumber] = i+1; - } - } - - var newIdx = SPECIAL.lastIndex; - idx = newIdx; - var code = result[0]; - if (code === "%") { // comment character - var newLinePos = text.indexOf("\n", idx); - if (newLinePos === -1) { - newLinePos = text.length; - }; - var commentString = text.substring(idx, newLinePos); - if (commentString.indexOf("%novalidate") === 0) { - return []; - } else if(!checkingDisabled && commentString.indexOf("%begin novalidate") === 0) { - checkingDisabled = true; - } else if (checkingDisabled && commentString.indexOf("%end novalidate") === 0) { - checkingDisabled = false; - }; - idx = SPECIAL.lastIndex = newLinePos + 1; - Comments.push([lineNumber, idx, newLinePos]); - lineNumber++; - linePosition[lineNumber] = idx; - } else if (checkingDisabled) { - continue; - } else if (code === '\\') { // escape character - NEXTCS.lastIndex = idx; - var controlSequence = NEXTCS.exec(text); - var nextSpecialPos = controlSequence === null ? idx : controlSequence.index; - if (nextSpecialPos === idx) { - Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]); - idx = SPECIAL.lastIndex = idx + 1; - char = text[nextSpecialPos]; - if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;}; - } else { - Tokens.push([lineNumber, code, pos, nextSpecialPos, text.slice(idx, nextSpecialPos)]); - var char; - while ((char = text[nextSpecialPos]) === ' ' || char === '\t' || char === '\r' || char === '\n') { - nextSpecialPos++; - if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;}; - } - idx = SPECIAL.lastIndex = nextSpecialPos; - } - } else if (code === "{") { // open group - Tokens.push([lineNumber, code, pos]); - } else if (code === "}") { // close group - Tokens.push([lineNumber, code, pos]); - } else if (code === "$") { // math mode - Tokens.push([lineNumber, code, pos]); - } else if (code === "&") { // tabalign - Tokens.push([lineNumber, code, pos]); - } else if (code === "#") { // macro parameter - Tokens.push([lineNumber, code, pos]); - } else if (code === "^") { // superscript - Tokens.push([lineNumber, code, pos]); - } else if (code === "_") { // subscript - Tokens.push([lineNumber, code, pos]); - } else if (code === "~") { // active character (space) - Tokens.push([lineNumber, code, pos]); - } else { - throw "unrecognised character " + code; - } - } - - return {tokens: Tokens, comments: Comments, linePosition: linePosition, lineNumber: lineNumber, text: text}; -}; - -var read1arg = function (TokeniseResult, k, options) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - if (options && options.allowStar) { - var optional = Tokens[k+1]; - if (optional && optional[1] === "Text") { - var optionalstr = text.substring(optional[2], optional[3]); - if (optionalstr === "*") { k++;} - }; - }; - - var open = Tokens[k+1]; - var env = Tokens[k+2]; - var close = Tokens[k+3]; - var envName; - - if(open && open[1] === "\\") { - envName = open[4]; // array element 4 is command sequence - return k + 1; - } else if(open && open[1] === "{" && env && env[1] === "\\" && close && close[1] === "}") { - envName = env[4]; // NOTE: if we were actually using this, keep track of * above - return k + 3; // array element 4 is command sequence - } else { - return null; - } -}; - -var readLetDefinition = function (TokeniseResult, k) { - - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var first = Tokens[k+1]; - var second = Tokens[k+2]; - var third = Tokens[k+3]; - - if(first && first[1] === "\\" && second && second[1] === "\\") { - return k + 2; - } else if(first && first[1] === "\\" && - second && second[1] === "Text" && text.substring(second[2], second[3]) === "=" && - third && third[1] === "\\") { - return k + 3; - } else { - return null; - } -}; - -var read1name = function (TokeniseResult, k) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var open = Tokens[k+1]; - var env = Tokens[k+2]; - var close = Tokens[k+3]; - - if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") { - var envName = text.substring(env[2], env[3]); - return k + 3; - } else if (open && open[1] === "{" && env && env[1] === "Text") { - envName = ""; - for (var j = k + 2, tok; (tok = Tokens[j]); j++) { - if (tok[1] === "Text") { - var str = text.substring(tok[2], tok[3]); - if (!str.match(/^\S*$/)) { break; } - envName = envName + str; - } else if (tok[1] === "_") { - envName = envName + "_"; - } else { - break; - } - } - if (tok && tok[1] === "}") { - return j; // advance past these tokens - } else { - return null; - } - } else { - return null; - } -}; - -var read1filename = function (TokeniseResult, k) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var fileName = ""; - for (var j = k + 1, tok; (tok = Tokens[j]); j++) { - if (tok[1] === "Text") { - var str = text.substring(tok[2], tok[3]); - if (!str.match(/^\S*$/)) { break; } - fileName = fileName + str; - } else if (tok[1] === "_") { - fileName = fileName + "_"; - } else { - break; - } - } - if (fileName.length > 0) { - return j; // advance past these tokens - } else { - return null; - } -}; - -var readOptionalParams = function(TokeniseResult, k) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var params = Tokens[k+1]; - - if(params && params[1] === "Text") { - var paramNum = text.substring(params[2], params[3]); - if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) { - return k + 1; // got it - }; - }; - var count = 0; - var nextToken = Tokens[k+1]; - var pos = nextToken[2]; - - for (var i = pos, end = text.length; i < end; i++) { - var char = text[i]; - if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; - if (char === "[") { count++; } - if (char === "]") { count--; } - if (count === 0 && char === "{") { return k - 1; } - if (count > 0 && (char === '\r' || char === '\n')) { return null; } - }; - return null; -}; - -var readOptionalGeneric = function(TokeniseResult, k) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var params = Tokens[k+1]; - - if(params && params[1] === "Text") { - var paramNum = text.substring(params[2], params[3]); - if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) { - return k + 1; // got it - }; - }; - return null; -}; - -var readOptionalDef = function (TokeniseResult, k) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var defToken = Tokens[k]; - var pos = defToken[3]; - - var openBrace = "{"; - var nextToken = Tokens[k+1]; - for (var i = pos, end = text.length; i < end; i++) { - var char = text[i]; - if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; - if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments - if (char === '\r' || char === '\n') { return null; } - }; - - return null; - -}; - -var readDefinition = function(TokeniseResult, k) { - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - k = k + 1; - var count = 0; - var nextToken = Tokens[k]; - while (nextToken && nextToken[1] === "Text") { - var start = nextToken[2], end = nextToken[3]; - for (var i = start; i < end; i++) { - var char = text[i]; - if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { continue; } - return null; // bail out, should begin with a { - } - k++; - nextToken = Tokens[k]; - } - if (nextToken && nextToken[1] === "{") { - count++; - while (count>0) { - k++; - nextToken = Tokens[k]; - if(!nextToken) { break; }; - if (nextToken[1] === "}") { count--; } - if (nextToken[1] === "{") { count++; } - } - return k; - } - - return null; -}; - -var readVerb = function(TokeniseResult, k) { - - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var verbToken = Tokens[k]; - var verbStr = text.substring(verbToken[2], verbToken[3]); - var pos = verbToken[3]; - if (text[pos] === "*") { pos++; } // \verb* form of command - var delimiter = text[pos]; - pos++; - - var nextToken = Tokens[k+1]; - for (var i = pos, end = text.length; i < end; i++) { - var char = text[i]; - if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; - if (char === delimiter) { return k; }; - if (char === '\r' || char === '\n') { return null; } - }; - - return null; -}; - -var readUrl = function(TokeniseResult, k) { - - var Tokens = TokeniseResult.tokens; - var text = TokeniseResult.text; - - var urlToken = Tokens[k]; - var urlStr = text.substring(urlToken[2], urlToken[3]); - var pos = urlToken[3]; - var openDelimiter = text[pos]; - var closeDelimiter = (openDelimiter === "{") ? "}" : openDelimiter; - var nextToken = Tokens[k+1]; - if (nextToken && pos === nextToken[2]) { - k++; - nextToken = Tokens[k+1]; - }; - pos++; - - var count = 1; - for (var i = pos, end = text.length; count > 0 && i < end; i++) { - var char = text[i]; - if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];}; - if (char === closeDelimiter) { - count--; - } else if (char === openDelimiter) { - count++; - }; - if (count === 0) { return k; }; - if (char === '\r' || char === '\n') { return null; } - }; - - return null; -}; - -var InterpretTokens = function (TokeniseResult, ErrorReporter) { - var Tokens = TokeniseResult.tokens; - var linePosition = TokeniseResult.linePosition; - var lineNumber = TokeniseResult.lineNumber; - var text = TokeniseResult.text; - - var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo; - var TokenError = ErrorReporter.TokenError; - var Environments = new EnvHandler(ErrorReporter); - - var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox) - var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes - var seenUserDefinedBeginEquation = false; // if we have seen macros like \beq - var seenUserDefinedEndEquation = false; // if we have seen macros like \eeq - - for (var i = 0, len = Tokens.length; i < len; i++) { - var token = Tokens[i]; - var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4]; - - if (type === "{") { - Environments.push({command:"{", token:token, mathMode: nextGroupMathMode}); - nextGroupMathModeStack.push(nextGroupMathMode); - nextGroupMathMode = null; - continue; - } else if (type === "}") { - Environments.push({command:"}", token:token}); - nextGroupMathMode = nextGroupMathModeStack.pop(); - continue; - } else { - nextGroupMathMode = null; - }; - - if (type === "\\") { - if (seq === "begin" || seq === "end") { - var open = Tokens[i+1]; - var env = Tokens[i+2]; - var close = Tokens[i+3]; - if(open && open[1] === "{" && env && env[1] === "Text" && close && close[1] === "}") { - var envName = text.substring(env[2], env[3]); - Environments.push({command: seq, name: envName, token: token, closeToken: close}); - i = i + 3; // advance past these tokens - } else { - if (open && open[1] === "{" && env && env[1] === "Text") { - envName = ""; - for (var j = i + 2, tok; (tok = Tokens[j]); j++) { - if (tok[1] === "Text") { - var str = text.substring(tok[2], tok[3]); - if (!str.match(/^\S*$/)) { break; } - envName = envName + str; - } else if (tok[1] === "_") { - envName = envName + "_"; - } else { - break; - } - } - if (tok && tok[1] === "}") { - Environments.push({command: seq, name: envName, token: token, closeToken: close}); - i = j; // advance past these tokens - continue; - } - } - var endToken = null; - if (open && open[1] === "{") { - endToken = open; // we've got a { - if (env && env[1] === "Text") { - endToken = env.slice(); // we've got some text following the { - start = endToken[2]; end = endToken[3]; - for (j = start; j < end; j++) { - var char = text[j]; - if (char === ' ' || char === '\t' || char === '\r' || char === '\n') { break; } - } - endToken[3] = j; // the end of partial token is as far as we got looking ahead - }; - }; - - if (endToken) { - TokenErrorFromTo(token, endToken, "invalid environment command " + text.substring(token[2], endToken[3] || endToken[2])); - } else { - TokenError(token, "invalid environment command"); - }; - } - } else if (typeof seq === "string" && seq.match(/^(be|beq|beqa|bea)$/i)) { - seenUserDefinedBeginEquation = true; - } else if (typeof seq === "string" && seq.match(/^(ee|eeq|eeqn|eeqa|eeqan|eea)$/i)) { - seenUserDefinedEndEquation = true; - } else if (seq === "newcommand" || seq === "renewcommand" || seq === "DeclareRobustCommand") { - var newPos = read1arg(TokeniseResult, i, {allowStar: true}); - if (newPos === null) { continue; } else {i = newPos;}; - newPos = readOptionalParams(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - - } else if (seq === "def") { - newPos = read1arg(TokeniseResult, i); - if (newPos === null) { continue; } else {i = newPos;}; - newPos = readOptionalDef(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - - } else if (seq === "let") { - newPos = readLetDefinition(TokeniseResult, i); - if (newPos === null) { continue; } else {i = newPos;}; - - } else if (seq === "newcolumntype") { - newPos = read1name(TokeniseResult, i); - if (newPos === null) { continue; } else {i = newPos;}; - newPos = readOptionalParams(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - - } else if (seq === "newenvironment" || seq === "renewenvironment") { - newPos = read1name(TokeniseResult, i); - if (newPos === null) { continue; } else {i = newPos;}; - newPos = readOptionalParams(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - } else if (seq === "verb") { - newPos = readVerb(TokeniseResult, i); - if (newPos === null) { TokenError(token, "invalid verbatim command"); } else {i = newPos;}; - } else if (seq === "url") { - newPos = readUrl(TokeniseResult, i); - if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;}; - } else if (seq === "left" || seq === "right") { - var nextToken = Tokens[i+1]; - char = ""; - if (nextToken && nextToken[1] === "Text") { - char = text.substring(nextToken[2], nextToken[2] + 1); - } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") { - char = nextToken[4]; - } else if (nextToken && nextToken[1] === "\\") { - char = "unknown"; - } - if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) { - TokenError(token, "invalid bracket command"); - } else { - i = i + 1; - Environments.push({command:seq, token:token}); - }; - } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") { - Environments.push({command:seq, token:token}); - } else if (seq === "input") { - newPos = read1filename(TokeniseResult, i); - if (newPos === null) { continue; } else {i = newPos;}; - } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") { - nextGroupMathMode = false; - } else if (seq === "rotatebox" || seq === "scalebox") { - newPos = readOptionalGeneric(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - nextGroupMathMode = false; - } else if (seq === "resizebox") { - newPos = readOptionalGeneric(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - - nextGroupMathMode = false; - } else if (seq === "DeclareMathOperator") { - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - } else if (seq === "DeclarePairedDelimiter") { - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - newPos = readDefinition(TokeniseResult, i); - if (newPos === null) { /* do nothing */ } else {i = newPos;}; - } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) { - var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (currentMathMode === null) { - TokenError(token, type + seq + " must be inside math mode", {mathMode:true}); - }; - } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) { - currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (currentMathMode) { - TokenError(token, type + seq + " used inside math mode", {mathMode:true}); - Environments.resetMathMode(); - }; - } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) { - nextGroupMathMode = undefined; - }; - - } else if (type === "$") { - var lookAhead = Tokens[i+1]; - var nextIsDollar = lookAhead && lookAhead[1] === "$"; - currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) { - Environments.push({command:"$$", token:token}); - i = i + 1; - } else { - Environments.push({command:"$", token:token}); - } - } else if (type === "^" || type === "_") { - currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display) - var insideGroup = Environments.insideGroup(); // true if inside {....} - if (currentMathMode === null && !insideGroup) { - TokenError(token, type + " must be inside math mode", {mathMode:true}); - }; - } - }; - - if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) { - ErrorReporter.filterMath = true; - }; - - return Environments; -}; - -var EnvHandler = function (ErrorReporter) { - var ErrorTo = ErrorReporter.EnvErrorTo; - var ErrorFromTo = ErrorReporter.EnvErrorFromTo; - var ErrorFrom = ErrorReporter.EnvErrorFrom; - - var envs = []; - - var state = []; - var documentClosed = null; - var inVerbatim = false; - var verbatimRanges = []; - - this.Environments = envs; - - this.push = function (newEnv) { - this.setEnvProps(newEnv); - this.checkAndUpdateState(newEnv); - envs.push(newEnv); - }; - - this._endVerbatim = function (thisEnv) { - var lastEnv = state.pop(); - if (lastEnv && lastEnv.name === thisEnv.name) { - inVerbatim = false; - verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]}); - } else { - if(lastEnv) { state.push(lastEnv); } ; - } - }; - - var invalidEnvs = []; - - this._end = function (thisEnv) { - do { - var lastEnv = state.pop(); - var retry = false; - var i; - - if (closedBy(lastEnv, thisEnv)) { - if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) { - documentClosed = thisEnv; - }; - return; - } else if (!lastEnv) { - if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"}); - } else { - ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); - } - } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) { - invalidEnvs.splice(i, 1); - if (lastEnv) { state.push(lastEnv); } ; - return; - } else { - var status = reportError(lastEnv, thisEnv); - if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) { - invalidEnvs.push(lastEnv); - retry = true; - } else { - var prevLastEnv = state.pop(); - if(prevLastEnv) { - if (thisEnv.name === prevLastEnv.name) { - return; - } else { - state.push(prevLastEnv); - } - } - invalidEnvs.push(lastEnv); - } - - } - } while (retry === true); - }; - - var CLOSING_DELIMITER = { - "{" : "}", - "left" : "right", - "[" : "]", - "(" : ")", - "$" : "$", - "$$": "$$" - }; - - var closedBy = function (lastEnv, thisEnv) { - if (!lastEnv) { - return false ; - } else if (thisEnv.command === "end") { - return lastEnv.command === "begin" && lastEnv.name === thisEnv.name; - } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) { - return true; - } else { - return false; - } - }; - - var indexOfClosingEnvInArray = function (envs, thisEnv) { - for (var i = 0, n = envs.length; i < n ; i++) { - if (closedBy(envs[i], thisEnv)) { - return i; - } - } - return -1; - }; - - var envPrecedence = function (env) { - var openScore = { - "{" : 1, - "left" : 2, - "$" : 3, - "$$" : 4, - "begin": 4 - }; - var closeScore = { - "}" : 1, - "right" : 2, - "$" : 3, - "$$" : 5, - "end": 4 - }; - if (env.command) { - return openScore[env.command] || closeScore[env.command]; - } else { - return 0; - } - }; - - var getName = function(env) { - var description = { - "{" : "open group {", - "}" : "close group }", - "[" : "open display math \\[", - "]" : "close display math \\]", - "(" : "open inline math \\(", - ")" : "close inline math \\)", - "$" : "$", - "$$" : "$$", - "left" : "\\left", - "right" : "\\right" - }; - if (env.command === "begin" || env.command === "end") { - return "\\" + env.command + "{" + env.name + "}"; - } else if (env.command in description) { - return description[env.command]; - } else { - return env.command; - } - }; - - var EXTRA_CLOSE = 1; - var UNCLOSED_GROUP = 2; - var UNCLOSED_ENV = 3; - - var reportError = function(lastEnv, thisEnv) { - if (!lastEnv) { // unexpected close, nothing was open! - if (documentClosed) { - ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"}); - } else { - ErrorTo(thisEnv, "unexpected " + getName(thisEnv)); - }; - return EXTRA_CLOSE; - } else if (lastEnv.command === "{" && thisEnv.command === "end") { - ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), - {suppressIfEditing:true, errorAtStart: true, type:"warning"}); - return UNCLOSED_GROUP; - } else { - var pLast = envPrecedence(lastEnv); - var pThis = envPrecedence(thisEnv); - if (pThis > pLast) { - ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv), - {suppressIfEditing:true, errorAtStart: true}); - } else { - ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv)); - } - return UNCLOSED_ENV; - }; - }; - - this._beginMathMode = function (thisEnv) { - var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env - if (currentMathMode) { - ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true, mathMode:true}); - }; - thisEnv.mathMode = thisEnv; - state.push(thisEnv); - }; - - this._toggleMathMode = function (thisEnv) { - var lastEnv = state.pop(); - if (closedBy(lastEnv, thisEnv)) { - return; - } else { - if (lastEnv) {state.push(lastEnv);} - if (lastEnv && lastEnv.mathMode) { - this._end(thisEnv); - } else { - thisEnv.mathMode = thisEnv; - state.push(thisEnv); - } - }; - }; - - this.getMathMode = function () { - var n = state.length; - if (n > 0) { - return state[n-1].mathMode; - } else { - return null; - } - }; - - this.insideGroup = function () { - var n = state.length; - if (n > 0) { - return (state[n-1].command === "{"); - } else { - return null; - } - }; - - var resetMathMode = function () { - var n = state.length; - if (n > 0) { - var lastMathMode = state[n-1].mathMode; - do { - var lastEnv = state.pop(); - } while (lastEnv && lastEnv !== lastMathMode); - } else { - return; - } - }; - - this.resetMathMode = resetMathMode; - - var getNewMathMode = function (currentMathMode, thisEnv) { - var newMathMode = null; - - if (thisEnv.command === "{") { - if (thisEnv.mathMode !== null) { - newMathMode = thisEnv.mathMode; - } else { - newMathMode = currentMathMode; - } - } else if (thisEnv.command === "left") { - if (currentMathMode === null) { - ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true}); - }; - newMathMode = currentMathMode; - } else if (thisEnv.command === "begin") { - var name = thisEnv.name; - if (name) { - if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) { - if (currentMathMode) { - ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true, mathMode: true}); - resetMathMode(); - }; - newMathMode = null; - } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) { - if (currentMathMode === null) { - ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true}); - }; - newMathMode = currentMathMode; - } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) { - if (currentMathMode) { - ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode), - {suppressIfEditing:true, errorAtStart: true, mathMode: true}); - resetMathMode(); - }; - newMathMode = thisEnv; - } else { - newMathMode = undefined; // undefined means we don't know if we are in math mode or not - } - } - }; - return newMathMode; - }; - - this.checkAndUpdateState = function (thisEnv) { - if (inVerbatim) { - if (thisEnv.command === "end") { - this._endVerbatim(thisEnv); - } else { - return; // ignore anything in verbatim environments - } - } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") { - if (thisEnv.verbatim) {inVerbatim = true;}; - var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env - var newMathMode = getNewMathMode(currentMathMode, thisEnv); - thisEnv.mathMode = newMathMode; - state.push(thisEnv); - } else if (thisEnv.command === "end") { - this._end(thisEnv); - } else if (thisEnv.command === "(" || thisEnv.command === "[") { - this._beginMathMode(thisEnv); - } else if (thisEnv.command === ")" || thisEnv.command === "]") { - this._end(thisEnv); - } else if (thisEnv.command === "}") { - this._end(thisEnv); - } else if (thisEnv.command === "right") { - this._end(thisEnv); - } else if (thisEnv.command === "$" || thisEnv.command === "$$") { - this._toggleMathMode(thisEnv); - } - }; - - this.close = function () { - while (state.length > 0) { - var thisEnv = state.pop(); - if (thisEnv.command === "{") { - ErrorFrom(thisEnv, "unclosed group {", {type:"warning"}); - } else { - ErrorFrom(thisEnv, "unclosed " + getName(thisEnv)); - } - } - var vlen = verbatimRanges.length; - var len = ErrorReporter.tokenErrors.length; - if (vlen >0 && len > 0) { - for (var i = 0; i < len; i++) { - var tokenError = ErrorReporter.tokenErrors[i]; - var startPos = tokenError.startPos; - var endPos = tokenError.endPos; - for (var j = 0; j < vlen; j++) { - if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) { - tokenError.ignore = true; - break; - } - } - } - } - }; - - this.setEnvProps = function (env) { - var name = env.name ; - if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) { - env.verbatim = true; - } - }; -}; -var ErrorReporter = function (TokeniseResult) { - var text = TokeniseResult.text; - var linePosition = TokeniseResult.linePosition; - var lineNumber = TokeniseResult.lineNumber; - - var errors = [], tokenErrors = []; - this.errors = errors; - this.tokenErrors = tokenErrors; - this.filterMath = false; - - this.getErrors = function () { - var returnedErrors = []; - for (var i = 0, len = tokenErrors.length; i < len; i++) { - if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); } - } - var allErrors = returnedErrors.concat(errors); - var result = []; - - var mathErrorCount = 0; - for (i = 0, len = allErrors.length; i < len; i++) { - if (allErrors[i].mathMode) { - mathErrorCount++; - } - if (mathErrorCount > 10) { - return []; - } - } - - if (this.filterMath && mathErrorCount > 0) { - for (i = 0, len = allErrors.length; i < len; i++) { - if (!allErrors[i].mathMode) { - result.push(allErrors[i]); - } - } - return result; - } else { - return allErrors; - } - }; - - this.TokenError = function (token, message, options) { - if(!options) { options = { suppressIfEditing:true } ; }; - var line = token[0], type = token[1], start = token[2], end = token[3]; - var start_col = start - linePosition[line]; - if (!end) { end = start + 1; } ; - var end_col = end - linePosition[line]; - tokenErrors.push({row: line, - column: start_col, - start_row:line, - start_col: start_col, - end_row:line, - end_col: end_col, - type:"error", - text:message, - startPos: start, - endPos: end, - suppressIfEditing:options.suppressIfEditing, - mathMode: options.mathMode}); - }; - - this.TokenErrorFromTo = function (fromToken, toToken, message, options) { - if(!options) { options = {suppressIfEditing:true } ; }; - var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3]; - var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3]; - if (!toEnd) { toEnd = toStart + 1;}; - var start_col = fromStart - linePosition[fromLine]; - var end_col = toEnd - linePosition[toLine]; - - tokenErrors.push({row: fromLine, - column: start_col, - start_row: fromLine, - start_col: start_col, - end_row: toLine, - end_col: end_col, - type:"error", - text:message, - startPos: fromStart, - endPos: toEnd, - suppressIfEditing:options.suppressIfEditing, - mathMode: options.mathMode}); - }; - - - this.EnvErrorFromTo = function (fromEnv, toEnv, message, options) { - if(!options) { options = {} ; }; - var fromToken = fromEnv.token, toToken = toEnv.closeToken || toEnv.token; - var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3]; - if (!toToken) {toToken = fromToken;}; - var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3]; - if (!toEnd) { toEnd = toStart + 1;}; - var start_col = fromStart - linePosition[fromLine]; - var end_col = toEnd - linePosition[toLine]; - errors.push({row: options.errorAtStart ? fromLine : toLine, - column: options.errorAtStart ? start_col: end_col, - start_row:fromLine, - start_col: start_col, - end_row:toLine, - end_col: end_col, - type: options.type ? options.type : "error", - text:message, - suppressIfEditing:options.suppressIfEditing, - mathMode: options.mathMode}); - }; - - this.EnvErrorTo = function (toEnv, message, options) { - if(!options) { options = {} ; }; - var token = toEnv.closeToken || toEnv.token; - var line = token[0], type = token[1], start = token[2], end = token[3]; - if (!end) { end = start + 1; }; - var end_col = end - linePosition[line]; - var err = {row: line, - column: end_col, - start_row:0, - start_col: 0, - end_row: line, - end_col: end_col, - type: options.type ? options.type : "error", - text:message, - mathMode: options.mathMode}; - errors.push(err); - }; - - this.EnvErrorFrom = function (env, message, options) { - if(!options) { options = {} ; }; - var token = env.token; - var line = token[0], type = token[1], start = token[2], end = token[3]; - var start_col = start - linePosition[line]; - var end_col = Infinity; - errors.push({row: line, - column: start_col, - start_row:line, - start_col: start_col, - end_row: lineNumber, - end_col: end_col, - type: options.type ? options.type : "error", - text:message, - mathMode: options.mathMode}); - }; -}; - -var Parse = function (text) { - var TokeniseResult = Tokenise(text); - var Reporter = new ErrorReporter(TokeniseResult); - var Environments = InterpretTokens(TokeniseResult, Reporter); - Environments.close(); - return Reporter.getErrors(); -}; - -(function() { - var disabled = false; - - this.onUpdate = function() { - if (disabled) { return ; }; - - var value = this.doc.getValue(); - var errors = []; - try { - if (value) - errors = Parse(value); - } catch (e) { - disabled = true; - errors = []; - } - this.sender.emit("lint", errors); - }; - -}).call(LatexWorker.prototype); - -}); - -ace.define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) { - -function Empty() {} - -if (!Function.prototype.bind) { - Function.prototype.bind = function bind(that) { // .length is 1 - var target = this; - if (typeof target != "function") { - throw new TypeError("Function.prototype.bind called on incompatible " + target); - } - var args = slice.call(arguments, 1); // for normal call - var bound = function () { - - if (this instanceof bound) { - - var result = target.apply( - this, - args.concat(slice.call(arguments)) - ); - if (Object(result) === result) { - return result; - } - return this; - - } else { - return target.apply( - that, - args.concat(slice.call(arguments)) - ); - - } - - }; - if(target.prototype) { - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - Empty.prototype = null; - } - return bound; - }; -} -var call = Function.prototype.call; -var prototypeOfArray = Array.prototype; -var prototypeOfObject = Object.prototype; -var slice = prototypeOfArray.slice; -var _toString = call.bind(prototypeOfObject.toString); -var owns = call.bind(prototypeOfObject.hasOwnProperty); -var defineGetter; -var defineSetter; -var lookupGetter; -var lookupSetter; -var supportsAccessors; -if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) { - defineGetter = call.bind(prototypeOfObject.__defineGetter__); - defineSetter = call.bind(prototypeOfObject.__defineSetter__); - lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); - lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); -} -if ([1,2].splice(0).length != 2) { - if(function() { // test IE < 9 to splice bug - see issue #138 - function makeArray(l) { - var a = new Array(l+2); - a[0] = a[1] = 0; - return a; - } - var array = [], lengthBefore; - - array.splice.apply(array, makeArray(20)); - array.splice.apply(array, makeArray(26)); - - lengthBefore = array.length; //46 - array.splice(5, 0, "XXX"); // add one element - - lengthBefore + 1 == array.length - - if (lengthBefore + 1 == array.length) { - return true;// has right splice implementation without bugs - } - }()) {//IE 6/7 - var array_splice = Array.prototype.splice; - Array.prototype.splice = function(start, deleteCount) { - if (!arguments.length) { - return []; - } else { - return array_splice.apply(this, [ - start === void 0 ? 0 : start, - deleteCount === void 0 ? (this.length - start) : deleteCount - ].concat(slice.call(arguments, 2))) - } - }; - } else {//IE8 - Array.prototype.splice = function(pos, removeCount){ - var length = this.length; - if (pos > 0) { - if (pos > length) - pos = length; - } else if (pos == void 0) { - pos = 0; - } else if (pos < 0) { - pos = Math.max(length + pos, 0); - } - - if (!(pos+removeCount < length)) - removeCount = length - pos; - - var removed = this.slice(pos, pos+removeCount); - var insert = slice.call(arguments, 2); - var add = insert.length; - if (pos === length) { - if (add) { - this.push.apply(this, insert); - } - } else { - var remove = Math.min(removeCount, length - pos); - var tailOldPos = pos + remove; - var tailNewPos = tailOldPos + add - remove; - var tailCount = length - tailOldPos; - var lengthAfterRemove = length - remove; - - if (tailNewPos < tailOldPos) { // case A - for (var i = 0; i < tailCount; ++i) { - this[tailNewPos+i] = this[tailOldPos+i]; - } - } else if (tailNewPos > tailOldPos) { // case B - for (i = tailCount; i--; ) { - this[tailNewPos+i] = this[tailOldPos+i]; - } - } // else, add == remove (nothing to do) - - if (add && pos === lengthAfterRemove) { - this.length = lengthAfterRemove; // truncate array - this.push.apply(this, insert); - } else { - this.length = lengthAfterRemove + add; // reserves space - for (i = 0; i < add; ++i) { - this[pos+i] = insert[i]; - } - } - } - return removed; - }; - } -} -if (!Array.isArray) { - Array.isArray = function isArray(obj) { - return _toString(obj) == "[object Array]"; - }; -} -var boxedString = Object("a"), - splitString = boxedString[0] != "a" || !(0 in boxedString); - -if (!Array.prototype.forEach) { - Array.prototype.forEach = function forEach(fun /*, thisp*/) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - thisp = arguments[1], - i = -1, - length = self.length >>> 0; - if (_toString(fun) != "[object Function]") { - throw new TypeError(); // TODO message - } - - while (++i < length) { - if (i in self) { - fun.call(thisp, self[i], i, object); - } - } - }; -} -if (!Array.prototype.map) { - Array.prototype.map = function map(fun /*, thisp*/) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - length = self.length >>> 0, - result = Array(length), - thisp = arguments[1]; - if (_toString(fun) != "[object Function]") { - throw new TypeError(fun + " is not a function"); - } - - for (var i = 0; i < length; i++) { - if (i in self) - result[i] = fun.call(thisp, self[i], i, object); - } - return result; - }; -} -if (!Array.prototype.filter) { - Array.prototype.filter = function filter(fun /*, thisp */) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - length = self.length >>> 0, - result = [], - value, - thisp = arguments[1]; - if (_toString(fun) != "[object Function]") { - throw new TypeError(fun + " is not a function"); - } - - for (var i = 0; i < length; i++) { - if (i in self) { - value = self[i]; - if (fun.call(thisp, value, i, object)) { - result.push(value); - } - } - } - return result; - }; -} -if (!Array.prototype.every) { - Array.prototype.every = function every(fun /*, thisp */) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - length = self.length >>> 0, - thisp = arguments[1]; - if (_toString(fun) != "[object Function]") { - throw new TypeError(fun + " is not a function"); - } - - for (var i = 0; i < length; i++) { - if (i in self && !fun.call(thisp, self[i], i, object)) { - return false; - } - } - return true; - }; -} -if (!Array.prototype.some) { - Array.prototype.some = function some(fun /*, thisp */) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - length = self.length >>> 0, - thisp = arguments[1]; - if (_toString(fun) != "[object Function]") { - throw new TypeError(fun + " is not a function"); - } - - for (var i = 0; i < length; i++) { - if (i in self && fun.call(thisp, self[i], i, object)) { - return true; - } - } - return false; - }; -} -if (!Array.prototype.reduce) { - Array.prototype.reduce = function reduce(fun /*, initial*/) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - length = self.length >>> 0; - if (_toString(fun) != "[object Function]") { - throw new TypeError(fun + " is not a function"); - } - if (!length && arguments.length == 1) { - throw new TypeError("reduce of empty array with no initial value"); - } - - var i = 0; - var result; - if (arguments.length >= 2) { - result = arguments[1]; - } else { - do { - if (i in self) { - result = self[i++]; - break; - } - if (++i >= length) { - throw new TypeError("reduce of empty array with no initial value"); - } - } while (true); - } - - for (; i < length; i++) { - if (i in self) { - result = fun.call(void 0, result, self[i], i, object); - } - } - - return result; - }; -} -if (!Array.prototype.reduceRight) { - Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) { - var object = toObject(this), - self = splitString && _toString(this) == "[object String]" ? - this.split("") : - object, - length = self.length >>> 0; - if (_toString(fun) != "[object Function]") { - throw new TypeError(fun + " is not a function"); - } - if (!length && arguments.length == 1) { - throw new TypeError("reduceRight of empty array with no initial value"); - } - - var result, i = length - 1; - if (arguments.length >= 2) { - result = arguments[1]; - } else { - do { - if (i in self) { - result = self[i--]; - break; - } - if (--i < 0) { - throw new TypeError("reduceRight of empty array with no initial value"); - } - } while (true); - } - - do { - if (i in this) { - result = fun.call(void 0, result, self[i], i, object); - } - } while (i--); - - return result; - }; -} -if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) { - Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) { - var self = splitString && _toString(this) == "[object String]" ? - this.split("") : - toObject(this), - length = self.length >>> 0; - - if (!length) { - return -1; - } - - var i = 0; - if (arguments.length > 1) { - i = toInteger(arguments[1]); - } - i = i >= 0 ? i : Math.max(0, length + i); - for (; i < length; i++) { - if (i in self && self[i] === sought) { - return i; - } - } - return -1; - }; -} -if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) { - Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) { - var self = splitString && _toString(this) == "[object String]" ? - this.split("") : - toObject(this), - length = self.length >>> 0; - - if (!length) { - return -1; - } - var i = length - 1; - if (arguments.length > 1) { - i = Math.min(i, toInteger(arguments[1])); - } - i = i >= 0 ? i : length - Math.abs(i); - for (; i >= 0; i--) { - if (i in self && sought === self[i]) { - return i; - } - } - return -1; - }; -} -if (!Object.getPrototypeOf) { - Object.getPrototypeOf = function getPrototypeOf(object) { - return object.__proto__ || ( - object.constructor ? - object.constructor.prototype : - prototypeOfObject - ); - }; -} -if (!Object.getOwnPropertyDescriptor) { - var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " + - "non-object: "; - Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { - if ((typeof object != "object" && typeof object != "function") || object === null) - throw new TypeError(ERR_NON_OBJECT + object); - if (!owns(object, property)) - return; - - var descriptor, getter, setter; - descriptor = { enumerable: true, configurable: true }; - if (supportsAccessors) { - var prototype = object.__proto__; - object.__proto__ = prototypeOfObject; - - var getter = lookupGetter(object, property); - var setter = lookupSetter(object, property); - object.__proto__ = prototype; - - if (getter || setter) { - if (getter) descriptor.get = getter; - if (setter) descriptor.set = setter; - return descriptor; - } - } - descriptor.value = object[property]; - return descriptor; - }; -} -if (!Object.getOwnPropertyNames) { - Object.getOwnPropertyNames = function getOwnPropertyNames(object) { - return Object.keys(object); - }; -} -if (!Object.create) { - var createEmpty; - if (Object.prototype.__proto__ === null) { - createEmpty = function () { - return { "__proto__": null }; - }; - } else { - createEmpty = function () { - var empty = {}; - for (var i in empty) - empty[i] = null; - empty.constructor = - empty.hasOwnProperty = - empty.propertyIsEnumerable = - empty.isPrototypeOf = - empty.toLocaleString = - empty.toString = - empty.valueOf = - empty.__proto__ = null; - return empty; - } - } - - Object.create = function create(prototype, properties) { - var object; - if (prototype === null) { - object = createEmpty(); - } else { - if (typeof prototype != "object") - throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'"); - var Type = function () {}; - Type.prototype = prototype; - object = new Type(); - object.__proto__ = prototype; - } - if (properties !== void 0) - Object.defineProperties(object, properties); - return object; - }; -} - -function doesDefinePropertyWork(object) { - try { - Object.defineProperty(object, "sentinel", {}); - return "sentinel" in object; - } catch (exception) { - } -} -if (Object.defineProperty) { - var definePropertyWorksOnObject = doesDefinePropertyWork({}); - var definePropertyWorksOnDom = typeof document == "undefined" || - doesDefinePropertyWork(document.createElement("div")); - if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { - var definePropertyFallback = Object.defineProperty; - } -} - -if (!Object.defineProperty || definePropertyFallback) { - var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: "; - var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: " - var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " + - "on this javascript engine"; - - Object.defineProperty = function defineProperty(object, property, descriptor) { - if ((typeof object != "object" && typeof object != "function") || object === null) - throw new TypeError(ERR_NON_OBJECT_TARGET + object); - if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) - throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); - if (definePropertyFallback) { - try { - return definePropertyFallback.call(Object, object, property, descriptor); - } catch (exception) { - } - } - if (owns(descriptor, "value")) { - - if (supportsAccessors && (lookupGetter(object, property) || - lookupSetter(object, property))) - { - var prototype = object.__proto__; - object.__proto__ = prototypeOfObject; - delete object[property]; - object[property] = descriptor.value; - object.__proto__ = prototype; - } else { - object[property] = descriptor.value; - } - } else { - if (!supportsAccessors) - throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); - if (owns(descriptor, "get")) - defineGetter(object, property, descriptor.get); - if (owns(descriptor, "set")) - defineSetter(object, property, descriptor.set); - } - - return object; - }; -} -if (!Object.defineProperties) { - Object.defineProperties = function defineProperties(object, properties) { - for (var property in properties) { - if (owns(properties, property)) - Object.defineProperty(object, property, properties[property]); - } - return object; - }; -} -if (!Object.seal) { - Object.seal = function seal(object) { - return object; - }; -} -if (!Object.freeze) { - Object.freeze = function freeze(object) { - return object; - }; -} -try { - Object.freeze(function () {}); -} catch (exception) { - Object.freeze = (function freeze(freezeObject) { - return function freeze(object) { - if (typeof object == "function") { - return object; - } else { - return freezeObject(object); - } - }; - })(Object.freeze); -} -if (!Object.preventExtensions) { - Object.preventExtensions = function preventExtensions(object) { - return object; - }; -} -if (!Object.isSealed) { - Object.isSealed = function isSealed(object) { - return false; - }; -} -if (!Object.isFrozen) { - Object.isFrozen = function isFrozen(object) { - return false; - }; -} -if (!Object.isExtensible) { - Object.isExtensible = function isExtensible(object) { - if (Object(object) === object) { - throw new TypeError(); // TODO message - } - var name = ''; - while (owns(object, name)) { - name += '?'; - } - object[name] = true; - var returnValue = owns(object, name); - delete object[name]; - return returnValue; - }; -} -if (!Object.keys) { - var hasDontEnumBug = true, - dontEnums = [ - "toString", - "toLocaleString", - "valueOf", - "hasOwnProperty", - "isPrototypeOf", - "propertyIsEnumerable", - "constructor" - ], - dontEnumsLength = dontEnums.length; - - for (var key in {"toString": null}) { - hasDontEnumBug = false; - } - - Object.keys = function keys(object) { - - if ( - (typeof object != "object" && typeof object != "function") || - object === null - ) { - throw new TypeError("Object.keys called on a non-object"); - } - - var keys = []; - for (var name in object) { - if (owns(object, name)) { - keys.push(name); - } - } - - if (hasDontEnumBug) { - for (var i = 0, ii = dontEnumsLength; i < ii; i++) { - var dontEnum = dontEnums[i]; - if (owns(object, dontEnum)) { - keys.push(dontEnum); - } - } - } - return keys; - }; - -} -if (!Date.now) { - Date.now = function now() { - return new Date().getTime(); - }; -} -var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + - "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + - "\u2029\uFEFF"; -if (!String.prototype.trim || ws.trim()) { - ws = "[" + ws + "]"; - var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), - trimEndRegexp = new RegExp(ws + ws + "*$"); - String.prototype.trim = function trim() { - return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); - }; -} - -function toInteger(n) { - n = +n; - if (n !== n) { // isNaN - n = 0; - } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - return n; -} - -function isPrimitive(input) { - var type = typeof input; - return ( - input === null || - type === "undefined" || - type === "boolean" || - type === "number" || - type === "string" - ); -} - -function toPrimitive(input) { - var val, valueOf, toString; - if (isPrimitive(input)) { - return input; - } - valueOf = input.valueOf; - if (typeof valueOf === "function") { - val = valueOf.call(input); - if (isPrimitive(val)) { - return val; - } - } - toString = input.toString; - if (typeof toString === "function") { - val = toString.call(input); - if (isPrimitive(val)) { - return val; - } - } - throw new TypeError(); -} -var toObject = function (o) { - if (o == null) { // this matches both null and undefined - throw new TypeError("can't convert "+o+" to object"); - } - return Object(o); -}; - -}); diff --git a/services/web/public/ol-favicon.ico b/services/web/public/ol-favicon.ico new file mode 100644 index 0000000000..6a14f7a5b9 Binary files /dev/null and b/services/web/public/ol-favicon.ico differ diff --git a/services/web/public/stylesheets/_style_includes.less b/services/web/public/stylesheets/_style_includes.less new file mode 100644 index 0000000000..a5c7589ef0 --- /dev/null +++ b/services/web/public/stylesheets/_style_includes.less @@ -0,0 +1,84 @@ +@import url(https://netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css); + +@import "core/mixins.less"; + +// Reset +@import "core/normalize.less"; +@import "core/print.less"; + +// Core CSS +@import "core/scaffolding.less"; +@import "core/type.less"; +@import "core/grid.less"; + +// Components +@import "components/tables.less"; +@import "components/forms.less"; +@import "components/buttons.less"; +@import "components/card.less"; +//@import "components/code.less"; +@import "components/component-animations.less"; +@import "components/glyphicons.less"; +@import "components/dropdowns.less"; +@import "components/button-groups.less"; +@import "components/input-groups.less"; +@import "components/navs.less"; +@import "components/navbar.less"; +@import "components/footer.less"; +//@import "components/breadcrumbs.less"; +//@import "components/pagination.less"; +@import "components/pager.less"; +@import "components/labels.less"; +//@import "components/badges.less"; +//@import "components/jumbotron.less"; +@import "components/thumbnails.less"; +@import "components/alerts.less"; +@import "components/progress-bars.less"; +// @import "components/media.less"; +// @import "components/list-group.less"; +// @import "components/panels.less"; +// @import "components/wells.less"; +@import "components/close.less"; +@import "components/fineupload.less"; +@import "components/hover.less"; + +// Components w/ JavaScript +@import "components/modals.less"; +@import "components/tooltip.less"; +@import "components/popovers.less"; +@import "components/carousel.less"; + +// ngTagsInput +@import "components/tags-input.less"; + +// Utility classes +@import "core/utilities.less"; +@import "core/responsive-utilities.less"; + +// ShareLaTeX app classes +@import "app/base.less"; +@import "app/account-settings.less"; +@import "app/beta-program.less"; +@import "app/about-page.less"; +@import "app/project-list.less"; +@import "app/editor.less"; +@import "app/homepage.less"; +@import "app/plans.less"; +@import "app/recurly.less"; +@import "app/bonus.less"; +@import "app/register.less"; +@import "app/blog.less"; +@import "app/features.less"; +@import "app/templates.less"; +@import "app/wiki.less"; +@import "app/translations.less"; +@import "app/contact-us.less"; +@import "app/subscription.less"; +@import "app/sprites.less"; +@import "app/invite.less"; +@import "app/review-features-page.less"; +@import "app/error-pages.less"; + +@import "../js/libs/pdfListView/TextLayer.css"; +@import "../js/libs/pdfListView/AnnotationsLayer.css"; +@import "../js/libs/pdfListView/HighlightsLayer.css"; diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 3c3aae945a..75b7121094 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -72,17 +72,17 @@ height: 100%; background-color: #FFF; } - .loading-screen-lion-container { + .loading-screen-brand-container { width: 15%; min-width: 200px; text-align: center; } - .loading-screen-lion { + .loading-screen-brand { position: relative; width: 100%; - padding-top: 86.2%; + padding-top: @editor-loading-logo-padding-top; height: 0; - background: url(/img/brand/lion-grey.svg) no-repeat bottom / 100%; + background: @editor-loading-logo-background-url no-repeat bottom / 100%; &::after { content: ''; @@ -91,7 +91,7 @@ right: 0; bottom: 0; left: 0; - background: url(/img/brand/lion.svg) no-repeat bottom / 100%; + background: @editor-loading-logo-foreground-url no-repeat bottom / 100%; transition: height .5s; } } @@ -504,3 +504,4 @@ height: auto; border-bottom: 1px solid @modal-header-border-color; } + diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 312b933b85..13b8b7e03f 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -57,6 +57,15 @@ } } +.rp-collapse-arrow() { + display: inline-block; + transform: rotateZ(0deg); + transition: transform 0.15s ease; + &-on { + transform: rotateZ(-90deg); + } +} + .triangle(@_, @width, @height, @color) { position: absolute; border-color: transparent; @@ -131,7 +140,6 @@ } position: relative; - height: @rp-toolbar-height; border-bottom: 1px solid @rp-border-grey; background-color: @rp-bg-dim-blue; text-align: center; @@ -144,6 +152,10 @@ text-align: right; flex-grow: 1; } + .review-panel-toolbar-icon-on { + margin-right: 5px; + color: @red; + } .review-panel-toolbar-label-disabled { cursor: auto; margin-right: 5px; @@ -151,6 +163,45 @@ .review-panel-toolbar-spinner { margin-left: 5px; } + .rp-tc-state { + position: absolute; + top: 100%; + left: 0; + right: 0; + overflow: hidden; + list-style: none; + padding: 0 5px; + margin: 0; + border-bottom: 1px solid @rp-border-grey; + background-color: @rp-bg-dim-blue; + text-align: left; + } + .rp-tc-state-collapse { + .rp-collapse-arrow; + margin-left: 5px; + } + .rp-tc-state-item { + display: flex; + align-items: center; + padding: 3px 0; + &:last-of-type { + padding-bottom: 5px; + } + } + .rp-tc-state-item-everyone { + border-bottom: 1px solid @rp-border-grey; + color: @red; + } + .rp-tc-state-item-name { + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: @rp-semibold-weight; + } + .rp-tc-state-item-name-disabled { + opacity: .35; + } .rp-entry-list { display: none; @@ -629,16 +680,9 @@ font-size: 0.9em; } .rp-overview-file-header-collapse { - display: inline-block; + .rp-collapse-arrow; float: left; - transform: rotateZ(0deg); - transition: transform 0.15s ease } - - .rp-overview-file-header-collapse-on { - transform: rotateZ(-90deg); - } - .rp-overview-file-entries { overflow: hidden; } @@ -771,6 +815,11 @@ background-color: #FFF; } } + + &:disabled + .rp-toggle-btn { + cursor: default; + opacity: .35; + } } .ace-editor-wrapper { diff --git a/services/web/public/stylesheets/components/navbar.less b/services/web/public/stylesheets/components/navbar.less index 9da0dfabc5..52a4a10029 100755 --- a/services/web/public/stylesheets/components/navbar.less +++ b/services/web/public/stylesheets/components/navbar.less @@ -386,7 +386,7 @@ bottom: 5px; width: 180px; padding: 0; - background-image: url('/img/brand/logo-horizontal.svg'); + background-image: @navbar-brand-image-url; background-size: contain; background-repeat: no-repeat; background-position: left center; diff --git a/services/web/public/stylesheets/components/navs.less b/services/web/public/stylesheets/components/navs.less index 262524f864..0616da1b8c 100755 --- a/services/web/public/stylesheets/components/navs.less +++ b/services/web/public/stylesheets/components/navs.less @@ -225,6 +225,10 @@ // Hide tabbable panes to start, show them when `.active` .tab-content { + background-color: @nav-tabs-active-link-hover-bg; + border: 1px solid @nav-tabs-border-color; + border-top: none; + padding: @line-height-computed / 2; > .tab-pane { display: none; } diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less new file mode 100644 index 0000000000..6274b265f9 --- /dev/null +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -0,0 +1,805 @@ +// +// Variables +// -------------------------------------------------- + +//== Scaffolding +// +// ## Settings for some of the most global styles. + +//** Background color for ``. +@body-bg: #fff; +//** Global text color on ``. +@text-color: @gray-dark; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700); +//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i); +@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i); + +@font-family-sans-serif: "Open Sans", sans-serif; +@font-family-serif: "Merriweather", serif; +//** Default monospace fonts for ``, ``, and `
`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          16px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            floor((@font-size-base * 2)); // ~36px
+@font-size-h2:            floor((@font-size-base * 1.6)); // ~30px
+@font-size-h3:            ceil((@font-size-base * 1.25)); // ~24px
+@font-size-h4:            ceil((@font-size-base * 1.1)); // ~18px
+@font-size-h5:            @font-size-base;
+@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.5625; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the ``.
+@headings-font-family:    @font-family-serif;
+@headings-font-weight:    500;
+@headings-line-height:    1.1;
+@headings-color:          @gray-dark;
+
+
+//-- Iconography
+//
+//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+@icon-font-path:          "../fonts/";
+@icon-font-name:          "glyphicons-halflings-regular";
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     5px;
+@padding-base-horizontal:   16px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.33;
+@line-height-small:         1.5;
+
+@border-radius-base:        3px;
+@border-radius-large:       5px;
+@border-radius-small:       2px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                #f5f5f5;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                700;
+
+@btn-default-color:              #333;
+@btn-default-bg:                 #fff;
+@btn-default-border:             #ccc;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             darken(@btn-primary-bg, 10%);
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             darken(@btn-success-bg, 10%);
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                darken(@btn-info-bg, 15%);
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             darken(@btn-warning-bg, 10%);
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              darken(@btn-danger-bg, 10%);
+
+@btn-link-disabled-color:        @gray-light;
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+@input-bg:                       #fff;
+//** `` background color
+@input-bg-disabled:              @gray-lighter;
+
+//** Text color for ``s
+@input-color:                    @gray;
+//** `` border color
+@input-border:                   #ccc;
+//** `` border radius
+@input-border-radius:            @border-radius-base;
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        @gray-light;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           @gray-lighter;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @gray-dark;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      #fff;
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         @brand-primary;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+// Note: Deprecated @dropdown-caret-color as of v3.1.0
+@dropdown-caret-color:           #000;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1010;
+@zindex-tooltip:           1030;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
+@screen-xs:                  480px;
+@screen-xs-min:              @screen-xs;
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             ((720px + @grid-gutter-width));
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            ((940px + @grid-gutter-width));
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      ((1140px + @grid-gutter-width));
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    60px;
+@navbar-margin-bottom:             0;
+@navbar-border-radius:             0;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             #777;
+@navbar-default-bg:                #fff;
+@navbar-default-border:            @gray-lighter;
+
+// Navbar links
+@navbar-default-link-color:                @link-color;
+@navbar-default-link-hover-color:          @link-hover-color;
+@navbar-default-link-hover-bg:             @link-hover-color;
+@navbar-default-link-active-color:         #fff;
+@navbar-default-link-active-bg:            @link-hover-color;
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           @link-hover-color;
+@navbar-default-toggle-border-color:       @link-color;
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @link-color;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+@nav-open-link-hover-color:                 #fff;
+
+//== Tabs
+@nav-tabs-border-color:                     #ddd;
+
+@nav-tabs-link-hover-border-color:          @link-color;
+
+@nav-tabs-active-link-hover-bg:             @body-bg;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   #ddd;
+
+@nav-tabs-justified-link-border-color:            #ddd;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   @gray-lighter;
+@jumbotron-heading-color:        inherit;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             darken(@brand-success, 20%);
+@state-success-bg:               lighten(@brand-success, 50%);
+@state-success-border:           darken(@brand-success, 5%);
+
+@state-info-text:                darken(@brand-info, 20%);
+@state-info-bg:                  lighten(@brand-info, 47%);
+@state-info-border:              darken(@brand-info, 7%);
+
+@state-warning-text:             darken(@brand-warning, 10%);
+@state-warning-bg:               lighten(@brand-warning, 45%);
+@state-warning-border:           @brand-warning;
+
+@state-danger-text:              darken(@brand-danger, 10%);
+@state-danger-bg:                lighten(@brand-danger, 50%);
+@state-danger-border:            darken(@brand-danger, 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #000;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                rgba(0,0,0,.2);
+//** Popover fallback border color
+@popover-fallback-border-color:       #ccc;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 #fff;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         20px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   rgba(0,0,0,.2);
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   #e5e5e5;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+@modal-footer-background-color:   @gray-lightest;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 white;
+@progress-border-color:       @gray-lighter;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list elements on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list elements
+@list-group-active-color:       @component-active-color;
+//** Background color of active list elements
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+@list-group-link-color:         #555;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          @state-success-text;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @state-success-bg;
+
+@panel-info-text:             @state-info-text;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @state-info-bg;
+
+@panel-warning-text:          @state-warning-text;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @state-warning-bg;
+
+@panel-danger-text:           @state-danger-text;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @state-danger-bg;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f5f5f5;
+@well-border:                 darken(@well-bg, 7%);
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           bold;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           bold;
+@close-color:                 #000;
+@close-text-shadow:           0 1px 0 #fff;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.125);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+
+
+//== Miscellaneous
+//
+//##
+
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+
+@content-margin-top: @line-height-computed;
+@content-margin-top: @line-height-computed;
+
+// Custom
+
+@left-menu-width: 260px;
+@left-menu-animation-duration: 0.35s;
+
+@toolbar-border-color: @gray-lighter;
+@file-tree-droppable-background-color: rgb(252, 231, 199);
+
+@editor-dark-background-color: #333;
+@editor-dark-toolbar-border-color: #222;
+@editor-dark-highlight-color: #FFA03A;
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
new file mode 100644
index 0000000000..23151bd9c4
--- /dev/null
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -0,0 +1,37 @@
+@ol-green:      #4A9F48;
+@ol-dark-green: #1C5B26;
+
+//== Colors
+//
+//## Gray and brand colors for use across Bootstrap.
+
+
+@gray-darker:           #252525;
+@gray-dark:             #505050;
+@gray:                  #7a7a7a;
+@gray-light:            #a4a4a4;
+@gray-lighter:          #cfcfcf;
+@gray-lightest:         #f0f0f0;
+
+@blue:                  #405ebf;
+@blueDark:              #040D2D;
+@green:                 #46a546;
+@red:                   #a93529;
+@yellow:                #A1A729;
+@orange:                #f89406;
+@pink:                  #c3325f;
+@purple:                #7a43b6;
+
+@brand-primary:         @ol-green;
+@brand-success:         @green;
+@brand-info:            @ol-dark-green;
+@brand-warning:         @orange;
+@brand-danger:          #E03A06;
+
+@navbar-brand-image-url: url(/img/ol-brand/logo-horizontal.png);
+
+@editor-loading-logo-padding-top: 115.44%;
+@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);
+@editor-loading-logo-foreground-url: url(/img/ol-brand/overleaf-o.svg);
+
+@import "./_common-variables.less";
diff --git a/services/web/public/stylesheets/core/variables.less b/services/web/public/stylesheets/core/variables.less
index 7c1876ef9a..5fd9bfeddc 100755
--- a/services/web/public/stylesheets/core/variables.less
+++ b/services/web/public/stylesheets/core/variables.less
@@ -1,8 +1,3 @@
-//
-// Variables
-// --------------------------------------------------
-
-
 //== Colors
 //
 //## Gray and brand colors for use across Bootstrap.
@@ -30,804 +25,10 @@
 @brand-warning:         @orange;
 @brand-danger:          #E03A06;
 
-//== Scaffolding
-//
-// ## Settings for some of the most global styles.
+@navbar-brand-image-url: url(/img/brand/logo-horizontal.svg);
 
-//** Background color for ``.
-@body-bg:               #fff;
-//** Global text color on ``.
-@text-color:            @gray-dark;
+@editor-loading-logo-padding-top: 86.2%;
+@editor-loading-logo-background-url: url(/img/brand/lion-grey.svg);
+@editor-loading-logo-foreground-url: url(/img/brand/lion.svg);
 
-//** Global textual link color.
-@link-color:            @brand-primary;
-//** Link hover color set via `darken()` function.
-@link-hover-color:      darken(@link-color, 15%);
-
-
-//== Typography
-//
-//## Font, line-height, and color for body text, headings, and more.
-
-@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
-//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700);
-//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i);
-@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
-
-@font-family-sans-serif:  "Open Sans", sans-serif;
-@font-family-serif:       "Merriweather", serif;
-//** Default monospace fonts for ``, ``, and `
`.
-@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
-@font-family-base:        @font-family-sans-serif;
-
-@font-size-base:          16px;
-@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
-@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
-
-@font-size-h1:            floor((@font-size-base * 2)); // ~36px
-@font-size-h2:            floor((@font-size-base * 1.6)); // ~30px
-@font-size-h3:            ceil((@font-size-base * 1.25)); // ~24px
-@font-size-h4:            ceil((@font-size-base * 1.1)); // ~18px
-@font-size-h5:            @font-size-base;
-@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
-
-//** Unit-less `line-height` for use in components like buttons.
-@line-height-base:        1.5625; // 20/14
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
-
-//** By default, this inherits from the ``.
-@headings-font-family:    @font-family-serif;
-@headings-font-weight:    500;
-@headings-line-height:    1.1;
-@headings-color:          @gray-dark;
-
-
-//-- Iconography
-//
-//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-@icon-font-path:          "../fonts/";
-@icon-font-name:          "glyphicons-halflings-regular";
-@icon-font-svg-id:        "glyphicons_halflingsregular";
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-@padding-base-vertical:     5px;
-@padding-base-horizontal:   16px;
-
-@padding-large-vertical:    10px;
-@padding-large-horizontal:  16px;
-
-@padding-small-vertical:    5px;
-@padding-small-horizontal:  10px;
-
-@padding-xs-vertical:       1px;
-@padding-xs-horizontal:     5px;
-
-@line-height-large:         1.33;
-@line-height-small:         1.5;
-
-@border-radius-base:        3px;
-@border-radius-large:       5px;
-@border-radius-small:       2px;
-
-//** Global color for active items (e.g., navs or dropdowns).
-@component-active-color:    #fff;
-//** Global background color for active items (e.g., navs or dropdowns).
-@component-active-bg:       @brand-primary;
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-@caret-width-base:          4px;
-//** Carets increase slightly in size for larger components.
-@caret-width-large:         5px;
-
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for ``s and ``s.
-@table-cell-padding:            8px;
-//** Padding for cells in `.table-condensed`.
-@table-condensed-cell-padding:  5px;
-
-//** Default background color used for all tables.
-@table-bg:                      transparent;
-//** Background color used for `.table-striped`.
-@table-bg-accent:               #f9f9f9;
-//** Background color used for `.table-hover`.
-@table-bg-hover:                #f5f5f5;
-@table-bg-active:               @table-bg-hover;
-
-//** Border color for table and cell borders.
-@table-border-color:            #ddd;
-
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-@btn-font-weight:                700;
-
-@btn-default-color:              #333;
-@btn-default-bg:                 #fff;
-@btn-default-border:             #ccc;
-
-@btn-primary-color:              #fff;
-@btn-primary-bg:                 @brand-primary;
-@btn-primary-border:             darken(@btn-primary-bg, 10%);
-
-@btn-success-color:              #fff;
-@btn-success-bg:                 @brand-success;
-@btn-success-border:             darken(@btn-success-bg, 10%);
-
-@btn-info-color:                 #fff;
-@btn-info-bg:                    @brand-info;
-@btn-info-border:                darken(@btn-info-bg, 15%);
-
-@btn-warning-color:              #fff;
-@btn-warning-bg:                 @brand-warning;
-@btn-warning-border:             darken(@btn-warning-bg, 10%);
-
-@btn-danger-color:               #fff;
-@btn-danger-bg:                  @brand-danger;
-@btn-danger-border:              darken(@btn-danger-bg, 10%);
-
-@btn-link-disabled-color:        @gray-light;
-
-
-//== Forms
-//
-//##
-
-//** `` background color
-@input-bg:                       #fff;
-//** `` background color
-@input-bg-disabled:              @gray-lighter;
-
-//** Text color for ``s
-@input-color:                    @gray;
-//** `` border color
-@input-border:                   #ccc;
-//** `` border radius
-@input-border-radius:            @border-radius-base;
-//** Border color for inputs on focus
-@input-border-focus:             #66afe9;
-
-//** Placeholder text color
-@input-color-placeholder:        @gray-light;
-
-//** Default `.form-control` height
-@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
-//** Large `.form-control` height
-@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
-//** Small `.form-control` height
-@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
-
-@legend-color:                   @gray-dark;
-@legend-border-color:            #e5e5e5;
-
-//** Background color for textual input addons
-@input-group-addon-bg:           @gray-lighter;
-//** Border color for textual input addons
-@input-group-addon-border-color: @input-border;
-
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-@dropdown-bg:                    #fff;
-//** Dropdown menu `border-color`.
-@dropdown-border:                rgba(0,0,0,.15);
-//** Dropdown menu `border-color` **for IE8**.
-@dropdown-fallback-border:       #ccc;
-//** Divider color for between dropdown items.
-@dropdown-divider-bg:            #e5e5e5;
-
-//** Dropdown link text color.
-@dropdown-link-color:            @gray-dark;
-//** Hover color for dropdown links.
-@dropdown-link-hover-color:      #fff;
-//** Hover background for dropdown links.
-@dropdown-link-hover-bg:         @brand-primary;
-
-//** Active dropdown menu item text color.
-@dropdown-link-active-color:     @component-active-color;
-//** Active dropdown menu item background color.
-@dropdown-link-active-bg:        @component-active-bg;
-
-//** Disabled dropdown menu item background color.
-@dropdown-link-disabled-color:   @gray-light;
-
-//** Text color for headers within dropdown menus.
-@dropdown-header-color:          @gray-light;
-
-// Note: Deprecated @dropdown-caret-color as of v3.1.0
-@dropdown-caret-color:           #000;
-
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-@zindex-navbar:            1000;
-@zindex-dropdown:          1000;
-@zindex-popover:           1010;
-@zindex-tooltip:           1030;
-@zindex-navbar-fixed:      1030;
-@zindex-modal-background:  1040;
-@zindex-modal:             1050;
-
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
-@screen-xs:                  480px;
-@screen-xs-min:              @screen-xs;
-@screen-phone:               @screen-xs-min;
-
-// Small screen / tablet
-// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1
-@screen-sm:                  768px;
-@screen-sm-min:              @screen-sm;
-@screen-tablet:              @screen-sm-min;
-
-// Medium screen / desktop
-// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1
-@screen-md:                  992px;
-@screen-md-min:              @screen-md;
-@screen-desktop:             @screen-md-min;
-
-// Large screen / wide desktop
-// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1
-@screen-lg:                  1200px;
-@screen-lg-min:              @screen-lg;
-@screen-lg-desktop:          @screen-lg-min;
-
-// So media queries don't overlap when required, provide a maximum
-@screen-xs-max:              (@screen-sm-min - 1);
-@screen-sm-max:              (@screen-md-min - 1);
-@screen-md-max:              (@screen-lg-min - 1);
-
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-@grid-columns:              12;
-//** Padding between columns. Gets divided in half for the left and right.
-@grid-gutter-width:         30px;
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-@grid-float-breakpoint:     @screen-sm-min;
-//** Point at which the navbar begins collapsing.
-@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
-
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-@container-tablet:             ((720px + @grid-gutter-width));
-//** For `@screen-sm-min` and up.
-@container-sm:                 @container-tablet;
-
-// Medium screen / desktop
-@container-desktop:            ((940px + @grid-gutter-width));
-//** For `@screen-md-min` and up.
-@container-md:                 @container-desktop;
-
-// Large screen / wide desktop
-@container-large-desktop:      ((1140px + @grid-gutter-width));
-//** For `@screen-lg-min` and up.
-@container-lg:                 @container-large-desktop;
-
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-@navbar-height:                    60px;
-@navbar-margin-bottom:             0;
-@navbar-border-radius:             0;
-@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
-@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
-@navbar-collapse-max-height:       340px;
-
-@navbar-default-color:             #777;
-@navbar-default-bg:                #fff;
-@navbar-default-border:            @gray-lighter;
-
-// Navbar links
-@navbar-default-link-color:                @link-color;
-@navbar-default-link-hover-color:          @link-hover-color;
-@navbar-default-link-hover-bg:             @link-hover-color;
-@navbar-default-link-active-color:         #fff;
-@navbar-default-link-active-bg:            @link-hover-color;
-@navbar-default-link-disabled-color:       #ccc;
-@navbar-default-link-disabled-bg:          transparent;
-
-// Navbar brand label
-@navbar-default-brand-color:               @navbar-default-link-color;
-@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
-@navbar-default-brand-hover-bg:            transparent;
-
-// Navbar toggle
-@navbar-default-toggle-hover-bg:           @link-hover-color;
-@navbar-default-toggle-border-color:       @link-color;
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-@nav-link-padding:                          10px 15px;
-@nav-link-hover-bg:                         @link-color;
-
-@nav-disabled-link-color:                   @gray-light;
-@nav-disabled-link-hover-color:             @gray-light;
-
-@nav-open-link-hover-color:                 #fff;
-
-//== Tabs
-@nav-tabs-border-color:                     #ddd;
-
-@nav-tabs-link-hover-border-color:          @link-color;
-
-@nav-tabs-active-link-hover-bg:             @body-bg;
-@nav-tabs-active-link-hover-color:          @gray;
-@nav-tabs-active-link-hover-border-color:   #ddd;
-
-@nav-tabs-justified-link-border-color:            #ddd;
-@nav-tabs-justified-active-link-border-color:     @body-bg;
-
-//== Pills
-@nav-pills-border-radius:                   @border-radius-base;
-@nav-pills-active-link-hover-bg:            @component-active-bg;
-@nav-pills-active-link-hover-color:         @component-active-color;
-
-
-//== Pagination
-//
-//##
-
-@pagination-color:                     @link-color;
-@pagination-bg:                        #fff;
-@pagination-border:                    #ddd;
-
-@pagination-hover-color:               @link-hover-color;
-@pagination-hover-bg:                  @gray-lighter;
-@pagination-hover-border:              #ddd;
-
-@pagination-active-color:              #fff;
-@pagination-active-bg:                 @brand-primary;
-@pagination-active-border:             @brand-primary;
-
-@pagination-disabled-color:            @gray-light;
-@pagination-disabled-bg:               #fff;
-@pagination-disabled-border:           #ddd;
-
-
-//== Pager
-//
-//##
-
-@pager-bg:                             @pagination-bg;
-@pager-border:                         @pagination-border;
-@pager-border-radius:                  15px;
-
-@pager-hover-bg:                       @pagination-hover-bg;
-
-@pager-active-bg:                      @pagination-active-bg;
-@pager-active-color:                   @pagination-active-color;
-
-@pager-disabled-color:                 @pagination-disabled-color;
-
-
-//== Jumbotron
-//
-//##
-
-@jumbotron-padding:              30px;
-@jumbotron-color:                inherit;
-@jumbotron-bg:                   @gray-lighter;
-@jumbotron-heading-color:        inherit;
-@jumbotron-font-size:            ceil((@font-size-base * 1.5));
-
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-@state-success-text:             darken(@brand-success, 20%);
-@state-success-bg:               lighten(@brand-success, 50%);
-@state-success-border:           darken(@brand-success, 5%);
-
-@state-info-text:                darken(@brand-info, 20%);
-@state-info-bg:                  lighten(@brand-info, 47%);
-@state-info-border:              darken(@brand-info, 7%);
-
-@state-warning-text:             darken(@brand-warning, 10%);
-@state-warning-bg:               lighten(@brand-warning, 45%);
-@state-warning-border:           @brand-warning;
-
-@state-danger-text:              darken(@brand-danger, 10%);
-@state-danger-bg:                lighten(@brand-danger, 50%);
-@state-danger-border:            darken(@brand-danger, 5%);
-
-
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-@tooltip-max-width:           200px;
-//** Tooltip text color
-@tooltip-color:               #fff;
-//** Tooltip background color
-@tooltip-bg:                  #000;
-@tooltip-opacity:             .9;
-
-//** Tooltip arrow width
-@tooltip-arrow-width:         5px;
-//** Tooltip arrow color
-@tooltip-arrow-color:         @tooltip-bg;
-
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-@popover-bg:                          #fff;
-//** Popover maximum width
-@popover-max-width:                   276px;
-//** Popover border color
-@popover-border-color:                rgba(0,0,0,.2);
-//** Popover fallback border color
-@popover-fallback-border-color:       #ccc;
-
-//** Popover title background color
-@popover-title-bg:                    darken(@popover-bg, 3%);
-
-//** Popover arrow width
-@popover-arrow-width:                 10px;
-//** Popover arrow color
-@popover-arrow-color:                 #fff;
-
-//** Popover outer arrow width
-@popover-arrow-outer-width:           (@popover-arrow-width + 1);
-//** Popover outer arrow color
-@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
-//** Popover outer arrow fallback color
-@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
-
-
-//== Labels
-//
-//##
-
-//** Default label background color
-@label-default-bg:            @gray-light;
-//** Primary label background color
-@label-primary-bg:            @brand-primary;
-//** Success label background color
-@label-success-bg:            @brand-success;
-//** Info label background color
-@label-info-bg:               @brand-info;
-//** Warning label background color
-@label-warning-bg:            @brand-warning;
-//** Danger label background color
-@label-danger-bg:             @brand-danger;
-
-//** Default label text color
-@label-color:                 #fff;
-//** Default text color of a linked label
-@label-link-hover-color:      #fff;
-
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-@modal-inner-padding:         20px;
-
-//** Padding applied to the modal title
-@modal-title-padding:         15px;
-//** Modal title line-height
-@modal-title-line-height:     @line-height-base;
-
-//** Background color of modal content area
-@modal-content-bg:                             #fff;
-//** Modal content border color
-@modal-content-border-color:                   rgba(0,0,0,.2);
-//** Modal content border color **for IE8**
-@modal-content-fallback-border-color:          #999;
-
-//** Modal backdrop background color
-@modal-backdrop-bg:           #000;
-//** Modal backdrop opacity
-@modal-backdrop-opacity:      .5;
-//** Modal header border color
-@modal-header-border-color:   #e5e5e5;
-//** Modal footer border color
-@modal-footer-border-color:   @modal-header-border-color;
-@modal-footer-background-color:   @gray-lightest;
-
-@modal-lg:                    900px;
-@modal-md:                    600px;
-@modal-sm:                    300px;
-
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-@alert-padding:               15px;
-@alert-border-radius:         @border-radius-base;
-@alert-link-font-weight:      bold;
-
-@alert-success-bg:            @state-success-bg;
-@alert-success-text:          @state-success-text;
-@alert-success-border:        @state-success-border;
-
-@alert-info-bg:               @state-info-bg;
-@alert-info-text:             @state-info-text;
-@alert-info-border:           @state-info-border;
-
-@alert-warning-bg:            @state-warning-bg;
-@alert-warning-text:          @state-warning-text;
-@alert-warning-border:        @state-warning-border;
-
-@alert-danger-bg:             @state-danger-bg;
-@alert-danger-text:           @state-danger-text;
-@alert-danger-border:         @state-danger-border;
-
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-@progress-bg:                 white;
-@progress-border-color:       @gray-lighter;
-//** Progress bar text color
-@progress-bar-color:          #fff;
-
-//** Default progress bar color
-@progress-bar-bg:             @brand-primary;
-//** Success progress bar color
-@progress-bar-success-bg:     @brand-success;
-//** Warning progress bar color
-@progress-bar-warning-bg:     @brand-warning;
-//** Danger progress bar color
-@progress-bar-danger-bg:      @brand-danger;
-//** Info progress bar color
-@progress-bar-info-bg:        @brand-info;
-
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-@list-group-bg:                 #fff;
-//** `.list-group-item` border color
-@list-group-border:             #ddd;
-//** List group border radius
-@list-group-border-radius:      @border-radius-base;
-
-//** Background color of single list elements on hover
-@list-group-hover-bg:           #f5f5f5;
-//** Text color of active list elements
-@list-group-active-color:       @component-active-color;
-//** Background color of active list elements
-@list-group-active-bg:          @component-active-bg;
-//** Border color of active list elements
-@list-group-active-border:      @list-group-active-bg;
-@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
-
-@list-group-link-color:         #555;
-@list-group-link-heading-color: #333;
-
-
-//== Panels
-//
-//##
-
-@panel-bg:                    #fff;
-@panel-body-padding:          15px;
-@panel-border-radius:         @border-radius-base;
-
-//** Border color for elements within panels
-@panel-inner-border:          #ddd;
-@panel-footer-bg:             #f5f5f5;
-
-@panel-default-text:          @gray-dark;
-@panel-default-border:        #ddd;
-@panel-default-heading-bg:    #f5f5f5;
-
-@panel-primary-text:          #fff;
-@panel-primary-border:        @brand-primary;
-@panel-primary-heading-bg:    @brand-primary;
-
-@panel-success-text:          @state-success-text;
-@panel-success-border:        @state-success-border;
-@panel-success-heading-bg:    @state-success-bg;
-
-@panel-info-text:             @state-info-text;
-@panel-info-border:           @state-info-border;
-@panel-info-heading-bg:       @state-info-bg;
-
-@panel-warning-text:          @state-warning-text;
-@panel-warning-border:        @state-warning-border;
-@panel-warning-heading-bg:    @state-warning-bg;
-
-@panel-danger-text:           @state-danger-text;
-@panel-danger-border:         @state-danger-border;
-@panel-danger-heading-bg:     @state-danger-bg;
-
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-@thumbnail-padding:           4px;
-//** Thumbnail background color
-@thumbnail-bg:                @body-bg;
-//** Thumbnail border color
-@thumbnail-border:            #ddd;
-//** Thumbnail border radius
-@thumbnail-border-radius:     @border-radius-base;
-
-//** Custom text color for thumbnail captions
-@thumbnail-caption-color:     @text-color;
-//** Padding around the thumbnail caption
-@thumbnail-caption-padding:   9px;
-
-
-//== Wells
-//
-//##
-
-@well-bg:                     #f5f5f5;
-@well-border:                 darken(@well-bg, 7%);
-
-
-//== Badges
-//
-//##
-
-@badge-color:                 #fff;
-//** Linked badge text color on hover
-@badge-link-hover-color:      #fff;
-@badge-bg:                    @gray-light;
-
-//** Badge text color in active nav link
-@badge-active-color:          @link-color;
-//** Badge background color in active nav link
-@badge-active-bg:             #fff;
-
-@badge-font-weight:           bold;
-@badge-line-height:           1;
-@badge-border-radius:         10px;
-
-
-//== Breadcrumbs
-//
-//##
-
-@breadcrumb-padding-vertical:   8px;
-@breadcrumb-padding-horizontal: 15px;
-//** Breadcrumb background color
-@breadcrumb-bg:                 #f5f5f5;
-//** Breadcrumb text color
-@breadcrumb-color:              #ccc;
-//** Text color of current page in the breadcrumb
-@breadcrumb-active-color:       @gray-light;
-//** Textual separator for between breadcrumb elements
-@breadcrumb-separator:          "/";
-
-
-//== Carousel
-//
-//##
-
-@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
-
-@carousel-control-color:                      #fff;
-@carousel-control-width:                      15%;
-@carousel-control-opacity:                    .5;
-@carousel-control-font-size:                  20px;
-
-@carousel-indicator-active-bg:                #fff;
-@carousel-indicator-border-color:             #fff;
-
-@carousel-caption-color:                      #fff;
-
-
-//== Close
-//
-//##
-
-@close-font-weight:           bold;
-@close-color:                 #000;
-@close-text-shadow:           0 1px 0 #fff;
-
-
-//== Code
-//
-//##
-
-@code-color:                  #c7254e;
-@code-bg:                     #f9f2f4;
-
-@kbd-color:                   #fff;
-@kbd-bg:                      #333;
-
-@pre-bg:                      #f5f5f5;
-@pre-color:                   @gray-dark;
-@pre-border-color:            #ccc;
-@pre-scrollable-max-height:   340px;
-
-
-//== Type
-//
-//##
-
-//** Text muted color
-@text-muted:                  @gray-light;
-//** Abbreviations and acronyms border color
-@abbr-border-color:           @gray-light;
-//** Headings small color
-@headings-small-color:        @gray-light;
-//** Blockquote small color
-@blockquote-small-color:      @gray;
-//** Blockquote font size
-@blockquote-font-size:        (@font-size-base * 1.125);
-//** Blockquote border color
-@blockquote-border-color:     @gray-lighter;
-//** Page header border color
-@page-header-border-color:    @gray-lighter;
-
-
-//== Miscellaneous
-//
-//##
-
-//** Horizontal line color.
-@hr-border:                   @gray-lighter;
-
-//** Horizontal offset for forms and lists.
-@component-offset-horizontal: 180px;
-
-@content-margin-top: @line-height-computed;
-@content-margin-top: @line-height-computed;
-
-// Custom
-
-@left-menu-width: 260px;
-@left-menu-animation-duration: 0.35s;
-
-@toolbar-border-color: @gray-lighter;
-@file-tree-droppable-background-color: rgb(252, 231, 199);
-
-@editor-dark-background-color: #333;
-@editor-dark-toolbar-border-color: #222;
-@editor-dark-highlight-color: #FFA03A;
+@import "_common-variables.less";
diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less
new file mode 100644
index 0000000000..ec53e5e8c8
--- /dev/null
+++ b/services/web/public/stylesheets/ol-style.less
@@ -0,0 +1,3 @@
+// Core variables and mixins
+@import "core/ol-variables.less";
+@import "_style_includes.less";
\ No newline at end of file
diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less
index b3e0c15294..760f378719 100755
--- a/services/web/public/stylesheets/style.less
+++ b/services/web/public/stylesheets/style.less
@@ -1,86 +1,3 @@
 // Core variables and mixins
 @import "core/variables.less";
-@import url(https://netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css);
-
-@import "core/mixins.less";
-
-// Reset
-@import "core/normalize.less";
-@import "core/print.less";
-
-// Core CSS
-@import "core/scaffolding.less";
-@import "core/type.less";
-@import "core/grid.less";
-
-// Components
-@import "components/tables.less";
-@import "components/forms.less";
-@import "components/buttons.less";
-@import "components/card.less";
-//@import "components/code.less";
-@import "components/component-animations.less";
-@import "components/glyphicons.less";
-@import "components/dropdowns.less";
-@import "components/button-groups.less";
-@import "components/input-groups.less";
-@import "components/navs.less";
-@import "components/navbar.less";
-@import "components/footer.less";
-//@import "components/breadcrumbs.less";
-//@import "components/pagination.less";
-@import "components/pager.less";
-@import "components/labels.less";
-//@import "components/badges.less";
-//@import "components/jumbotron.less";
-@import "components/thumbnails.less";
-@import "components/alerts.less";
-@import "components/progress-bars.less";
-// @import "components/media.less";
-// @import "components/list-group.less";
-// @import "components/panels.less";
-// @import "components/wells.less";
-@import "components/close.less";
-@import "components/fineupload.less";
-@import "components/hover.less";
-
-// Components w/ JavaScript
-@import "components/modals.less";
-@import "components/tooltip.less";
-@import "components/popovers.less";
-@import "components/carousel.less";
-
-// ngTagsInput
-@import "components/tags-input.less";
-
-// Utility classes
-@import "core/utilities.less";
-@import "core/responsive-utilities.less";
-
-// ShareLaTeX app classes
-@import "app/base.less";
-@import "app/account-settings.less";
-@import "app/beta-program.less";
-@import "app/about-page.less";
-@import "app/project-list.less";
-@import "app/editor.less";
-@import "app/homepage.less";
-@import "app/plans.less";
-@import "app/recurly.less";
-@import "app/bonus.less";
-@import "app/register.less";
-@import "app/blog.less";
-@import "app/features.less";
-@import "app/templates.less";
-@import "app/wiki.less";
-@import "app/translations.less";
-@import "app/contact-us.less";
-@import "app/subscription.less";
-@import "app/sprites.less";
-@import "app/invite.less";
-@import "app/review-features-page.less";
-@import "app/error-pages.less";
-
-@import	"../js/libs/pdfListView/TextLayer.css";
-@import	"../js/libs/pdfListView/AnnotationsLayer.css";
-@import	"../js/libs/pdfListView/HighlightsLayer.css";
+@import "_style_includes.less";
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee
index 0ebf9f8112..0a041a0865 100644
--- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee
@@ -116,6 +116,24 @@ describe "AuthenticationManager", ->
 					expect(err).to.exist
 					done()
 
+			it "should not start the bcrypt process", (done)->
+				@AuthenticationManager.setUserPassword @user_id, @password, (err)=>
+					@bcrypt.genSalt.called.should.equal false
+					@bcrypt.hash.called.should.equal false
+					done()
+
+		describe "too short", ->
+			beforeEach ->
+				@settings.passwordStrengthOptions =
+					length:
+						max:10
+						min:6
+				@password = "dsd"
+
+			it "should return and error", (done)->
+				@AuthenticationManager.setUserPassword @user_id, @password, (err)->
+					expect(err).to.exist
+					done()
 
 			it "should not start the bcrypt process", (done)->
 				@AuthenticationManager.setUserPassword @user_id, @password, (err)=>
diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee
index ffef94157b..87ff474a0a 100644
--- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee
@@ -37,6 +37,7 @@ describe "SubscriptionUpdater", ->
 			constructor: (opts)-> 
 				subscription.admin_id = opts.admin_id
 				return subscription
+			@remove: sinon.stub().yields()
 		@SubscriptionModel.update = @updateStub
 		@SubscriptionModel.findAndModify = @findAndModifyStub
 
@@ -230,3 +231,35 @@ describe "SubscriptionUpdater", ->
 				@ReferalAllocator.assignBonus.calledWith(@adminuser_id).should.equal true
 				done()
 
+	describe "deleteSubscription", ->
+		beforeEach (done) ->
+			@subscription_id = ObjectId().toString()
+			@subscription = {
+				"mock": "subscription",
+				admin_id: ObjectId(),
+				member_ids: [ ObjectId(), ObjectId(), ObjectId() ]
+			}
+			@SubscriptionLocator.getSubscription = sinon.stub().yields(null, @subscription)
+			@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().yields()
+			@SubscriptionUpdater.deleteSubscription @subscription_id, done
+			
+		it "should look up the subscription", ->
+			@SubscriptionLocator.getSubscription
+				.calledWith(@subscription_id)
+				.should.equal true
+		
+		it "should remove the subscription", ->
+			@SubscriptionModel.remove
+				.calledWith({_id: ObjectId(@subscription_id)})
+				.should.equal true
+		
+		it "should downgrade the admin_id", ->
+			@SubscriptionUpdater._setUsersMinimumFeatures
+				.calledWith(@subscription.admin_id)
+				.should.equal true
+		
+		it "should downgrade all of the members", ->
+			for user_id in @subscription.member_ids
+				@SubscriptionUpdater._setUsersMinimumFeatures
+					.calledWith(user_id)
+					.should.equal true
diff --git a/services/web/test/smoke/coffee/SmokeTests.coffee b/services/web/test/smoke/coffee/SmokeTests.coffee
index cdb63b1a7b..0e6f7d6244 100644
--- a/services/web/test/smoke/coffee/SmokeTests.coffee
+++ b/services/web/test/smoke/coffee/SmokeTests.coffee
@@ -94,7 +94,7 @@ describe "Opening", ->
 		
 			expect(error, "smoke test: error returned in getting project list").to.not.exist
 			expect(!!stderr.match("200 OK"), "smoke test: response code is not 200 getting project list").to.equal true
-			expect(!!stdout.match("Your Projects - ShareLaTeX, Online LaTeX Editor"), "smoke test: body does not have correct title").to.equal true
+			expect(!!stdout.match("Your Projects - .*, Online LaTeX Editor"), "smoke test: body does not have correct title").to.equal true
 			expect(!!stdout.match("ProjectPageController"), "smoke test: body does not have correct angular controller").to.equal true
 			done()