Merge branch 'master' into pr-fix-link-rewriting

This commit is contained in:
James Allen
2017-12-04 10:01:08 +00:00
56 changed files with 9430 additions and 626 deletions
+1
View File
@@ -73,3 +73,4 @@ Gemfile.lock
app/views/external
/modules/
docker-shared.yml
+1
View File
@@ -196,6 +196,7 @@ module.exports = (grunt) ->
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
"pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
"ace": "#{PackageVersions.lib('ace')}"
"fineuploader": "libs/#{PackageVersions.lib('fineuploader')}"
shim:
"pdfjs-dist/build/pdf":
deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"]
+14 -25
View File
@@ -42,7 +42,7 @@ pipeline {
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: '*/sk-unlisted-projects']], 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']]])
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']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-account-merge'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-account-merge.git']]])
}
@@ -70,6 +70,19 @@ pipeline {
sh 'ls -l node_modules/.bin'
}
}
stage('Unit Tests') {
steps {
sh 'make clean install' // Removes js files, so do before compile
sh 'make test_unit MOCHA_ARGS="--reporter=tap"'
}
}
stage('Acceptance Tests') {
steps {
sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"'
}
}
stage('Compile') {
agent {
@@ -109,30 +122,6 @@ pipeline {
}
}
stage('Unit Test') {
agent {
docker {
image 'node:6.9.5'
reuseNode true
}
}
steps {
sh 'env NODE_ENV=development ./node_modules/.bin/grunt mochaTest:unit --reporter=tap'
}
}
stage('Acceptance Tests') {
steps {
// This tagged relase of the acceptance test runner is a temporary fix
// to get the acceptance tests working before we move to a
// docker-compose workflow. See:
// https://github.com/sharelatex/web-sharelatex-internal/pull/148
sh 'docker pull sharelatex/sl-acceptance-test-runner:node-6.9-mongo-3.4'
sh 'docker run --rm -v $(pwd):/app --env SHARELATEX_ALLOW_PUBLIC_ACCESS=true sharelatex/sl-acceptance-test-runner:node-6.9-mongo-3.4 || (cat forever/app.log && false)'
}
}
stage('Package') {
steps {
sh 'rm -rf ./node_modules/grunt*'
+77
View File
@@ -0,0 +1,77 @@
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm npm -q
BUILD_NUMBER ?= local
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
PROJECT_NAME = web
all: install test
@echo "Run:"
@echo " make install to set up the project dependencies (in docker)"
@echo " make test to run all the tests for the project (in docker)"
add: docker-shared.yml
$(NPM) install --save ${P}
add_dev: docker-shared.yml
$(NPM) install --save-dev ${P}
install: docker-shared.yml
$(NPM) install
clean:
rm -f app.js
rm -rf app/js
rm -rf test/unit/js
rm -rf test/acceptance/js
for dir in modules/*; \
do \
rm -f $$dir/index.js; \
rm -rf $$dir/app/js; \
rm -rf $$dir/test/unit/js; \
rm -rf $$dir/test/acceptance/js; \
done
# Regenerate docker-shared.yml - not stictly a 'clean',
# but lets `make clean install` work nicely
bin/generate_volumes_file
# Deletes node_modules volume
docker-compose down --volumes
# Need regenerating if you change the web modules you have installed
docker-shared.yml:
bin/generate_volumes_file
test: test_unit test_acceptance
test_unit: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS}
test_acceptance: test_acceptance_app test_acceptance_modules
test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service
test_acceptance_app_start_service: test_acceptance_app_stop_service docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance
test_acceptance_app_stop_service: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo
test_acceptance_app_run: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS}
test_acceptance_modules: docker-shared.yml
# Break and error on any module failure
set -e; \
for dir in modules/*; \
do \
if [ -e $$dir/Makefile ]; then \
(make test_acceptance_module MODULE=$$dir) \
fi \
done
test_acceptance_module: docker-shared.yml
cd $(MODULE) && make test_acceptance
.PHONY:
all add install update test test_unit test_acceptance \
test_acceptance_start_service test_acceptance_stop_service \
test_acceptance_run
+63
View File
@@ -17,6 +17,69 @@ web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related
Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`).
New Docker-based build process
------------------------------
Note that the Grunt workflow from above should still work, but we are transitioning to a
Docker based testing workflow, which is documented below:
### Running the app
The app runs natively using npm and Node on the local system:
```
$ npm install
$ npm run start
```
*Ideally the app would run in Docker like the tests below, but with host networking not supported in OS X, we need to run it natively until all services are Dockerised.*
### Unit Tests
The test suites run in Docker.
Unit tests can be run in the `test_unit` container defined in `docker-compose.tests.yml`.
The makefile contains a short cut to run these:
```
make install # Only needs running once, or when npm packages are updated
make unit_test
```
During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI:
```
make unit_test MOCHA_ARGS='--grep=AuthorizationManager'
```
### Acceptance Tests
Acceptance tests are run against a live service, which runs in the `acceptance_test` container defined in `docker-compose.tests.yml`.
To run the tests out-of-the-box, the makefile defines:
```
make install # Only needs running once, or when npm packages are updated
make acceptance_test
```
However, during development it is often useful to leave the service running for rapid iteration on the acceptance tests. This can be done with:
```
make acceptance_test_start_service
make acceptance_test_run # Run as many times as needed during development
make acceptance_test_stop_service
```
`make acceptance_test` just runs these three commands in sequence.
During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI:
```
make acceptance_test_run MOCHA_ARGS='--grep=AuthorizationManager'
```
Unit test status
----------------
@@ -0,0 +1,28 @@
EditorRealTimeController = require "../Editor/EditorRealTimeController"
MetaHandler = require './MetaHandler'
logger = require 'logger-sharelatex'
module.exports = MetaController =
getMetadata: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "getting all labels for project"
MetaHandler.getAllMetaForProject project_id, (err, projectMeta) ->
if err?
logger.err {project_id, err}, "[MetaController] error getting all labels from project"
return next err
res.json {projectId: project_id, projectMeta: projectMeta}
broadcastMetadataForDoc: (req, res, next) ->
project_id = req.params.project_id
doc_id = req.params.doc_id
logger.log {project_id, doc_id}, "getting labels for doc"
MetaHandler.getMetaForDoc project_id, doc_id, (err, docMeta) ->
if err?
logger.err {project_id, doc_id, err}, "[MetaController] error getting labels from doc"
return next err
EditorRealTimeController.emitToRoom project_id, 'broadcastDocMeta', {
docId: doc_id, meta: docMeta
}
res.sendStatus 200
@@ -0,0 +1,66 @@
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
DocumentUpdaterHandler = require '../DocumentUpdater/DocumentUpdaterHandler'
packageMapping = require "./packageMapping"
module.exports = MetaHandler =
labelRegex: () ->
/\\label{(.{0,80}?)}/g
usepackageRegex: () ->
/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/g
ReqPackageRegex: () ->
/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/g
getAllMetaForProject: (projectId, callback=(err, projectMeta)->) ->
DocumentUpdaterHandler.flushProjectToMongo projectId, (err) ->
if err?
return callback err
ProjectEntityHandler.getAllDocs projectId, (err, docs) ->
if err?
return callback err
projectMeta = MetaHandler.extractMetaFromProjectDocs docs
callback null, projectMeta
getMetaForDoc: (projectId, docId, callback=(err, docMeta)->) ->
DocumentUpdaterHandler.flushDocToMongo projectId, docId, (err) ->
if err?
return callback err
ProjectEntityHandler.getDoc projectId, docId, (err, lines) ->
if err?
return callback err
docMeta = MetaHandler.extractMetaFromDoc lines
callback null, docMeta
extractMetaFromDoc: (lines) ->
docMeta = {labels: [], packages: {}}
packages = []
label_re = MetaHandler.labelRegex()
package_re = MetaHandler.usepackageRegex()
req_package_re = MetaHandler.ReqPackageRegex()
for line in lines
while labelMatch = label_re.exec line
if label = labelMatch[1]
docMeta.labels.push label
while packageMatch = package_re.exec line
if messy = packageMatch[1]
for pkg in messy.split ','
if clean = pkg.trim()
packages.push clean
while packageMatch = req_package_re.exec line
if messy = packageMatch[1]
for pkg in messy.split ','
if clean = pkg.trim()
packages.push clean
for pkg in packages
if packageMapping[pkg]?
docMeta.packages[pkg] = packageMapping[pkg]
return docMeta
extractMetaFromProjectDocs: (projectDocs) ->
projectMeta = {}
for _path, doc of projectDocs
projectMeta[doc._id] = MetaHandler.extractMetaFromDoc doc.lines
return projectMeta
File diff suppressed because one or more lines are too long
@@ -251,7 +251,7 @@ module.exports = ProjectController =
# Extract data from user's ObjectId
timestamp = parseInt(user_id.toString().substring(0, 8), 16)
rolloutPercentage = 10 # Percentage of users to roll out to
rolloutPercentage = 20 # Percentage of users to roll out to
if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
# Don't show if user is not part of roll out
return cb(null, { enabled: false, showOnboarding: false })
@@ -97,20 +97,39 @@ module.exports = SubscriptionController =
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
RecurlyWrapper.getSubscription subscription.recurlySubscription_id,
includeAccount: true,
(err, usersSubscription)->
# always render the page, but skip the recurly link if
# we can't get it for some reason
if err?
logger.err {err, userId: user._id}, "error getting billing details link from recurly, proceeding"
hostedLoginToken = usersSubscription?.account?.hosted_login_token
recurlySubdomain = Settings?.apis?.recurly?.subdomain
if err? || !hostedLoginToken || !recurlySubdomain
billingDetailsLink = null
else
billingDetailsLink = [
"https://",
recurlySubdomain,
".recurly.com/account/billing_info/edit?ht=",
hostedLoginToken
].join("")
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log {user, subscription, hasSubOrIsGroupMember, groupSubscriptions, billingDetailsLink}, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
billingDetailsLink: billingDetailsLink
userCustomSubscriptionPage: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
@@ -124,31 +143,6 @@ module.exports = SubscriptionController =
title: "your_subscription"
subscription: subscription
editBillingDetailsPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "update_billing_details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
updateBillingDetails: (req, res, next) ->
res.redirect "/user/subscription?saved_billing_details=true"
createSubscription: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
recurly_token_id = req.body.recurly_token_id
@@ -15,8 +15,6 @@ module.exports =
webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
webRouter.post '/user/subscription/billing-details/update', AuthenticationController.requireLogin(), SubscriptionController.updateBillingDetails
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
@@ -24,6 +24,7 @@ jsPath =
ace = PackageVersions.lib('ace')
pdfjs = PackageVersions.lib('pdfjs')
fineuploader = PackageVersions.lib('fineuploader')
getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}"
@@ -37,6 +38,7 @@ getFileContent = (filePath)->
logger.log "Generating file fingerprints..."
pathList = [
["#{jsPath}libs/#{fineuploader}.js"]
["#{jsPath}libs/require.js"]
["#{jsPath}ide.js"]
["#{jsPath}main.js"]
@@ -2,6 +2,7 @@ version = {
"pdfjs": "1.7.225"
"moment": "2.9.0"
"ace": "1.2.5"
"fineuploader": "5.15.4"
}
module.exports = {
+4
View File
@@ -43,6 +43,7 @@ SudoModeController = require('./Features/SudoMode/SudoModeController')
SudoModeMiddlewear = require('./Features/SudoMode/SudoModeMiddlewear')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
MetaController = require('./Features/Metadata/MetaController')
LabelsController = require('./Features/Labels/LabelsController')
TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
Features = require('./infrastructure/Features')
@@ -202,6 +203,9 @@ module.exports = class Router
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
webRouter.get '/project/:project_id/metadata', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.getMetadata
webRouter.post '/project/:project_id/doc/:doc_id/metadata', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.broadcastMetadataForDoc
webRouter.get '/project/:project_id/labels', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), LabelsController.getAllLabels
webRouter.post '/project/:project_id/doc/:doc_id/labels', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), LabelsController.broadcastLabelsForDoc
+2 -1
View File
@@ -132,7 +132,8 @@ html(itemscope, itemtype='http://schema.org/Product')
// minimal requirejs configuration (can be extended/overridden)
window.requirejs = {
"paths" : {
"moment": "libs/#{lib('moment')}"
"moment": "libs/#{lib('moment')}",
"fineuploader": "libs/#{lib('fineuploader')}"
},
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"config":{
+2 -3
View File
@@ -130,7 +130,8 @@ block requirejs
"moment": "libs/#{lib('moment')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"ace": "#{lib('ace')}"
"ace": "#{lib('ace')}",
"fineuploader": "libs/#{lib('fineuploader')}"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0,
@@ -164,5 +165,3 @@ block requirejs
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
src=buildJsPath('libs/require.js')
)
@@ -333,6 +333,7 @@ script(type='text/ng-template', id='newDocModalTemplate')
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type='text/ng-template', id='newFolderModalTemplate')
.modal-header
h3 #{translate("new_folder")}
@@ -363,6 +364,34 @@ script(type='text/ng-template', id='newFolderModalTemplate')
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type="text/template", id="qq-file-uploader-template")
div.qq-uploader-selector
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
div.qq-upload-button-selector.btn.btn-primary.btn-lg
div #{translate('upload')}
span.or.btn-lg #{translate('or')}
span.drag-here.btn-lg #{translate('drag_here')}
span.qq-drop-processing-selector
span #{translate('processing')}
span.qq-drop-processing-spinner-selector
ul.qq-upload-list-selector
li
div.qq-progress-bar-container-selector
div(
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
class="qq-progress-bar-selector qq-progress-bar"
)
span.qq-upload-file-selector.qq-upload-file
span.qq-upload-size-selector.qq-upload-size
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-header
h3 #{translate("upload_files")}
@@ -384,11 +413,7 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-body(
fine-upload
endpoint="/project/{{ project_id }}/upload"
waiting-for-response-text="{{inserting_files}}"
failed-upload-text="{{upload_failed_sorry}}"
upload-button-text="{{select_files}}"
drag-area-text="{{drag_files}}"
hint-text="{{hint_press_and_hold_control_key}}"
template-id="qq-file-uploader-template"
multiple="true"
auto-upload="false"
on-complete-callback="onComplete"
@@ -400,10 +425,10 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
control="control"
params="{'folder_id': parent_folder_id}"
)
span #{translate("upload_files")}
.modal-footer
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
script(type='text/ng-template', id='deleteEntityModalTemplate')
.modal-header
h3 #{translate("delete")} {{ entity.name }}
+1
View File
@@ -6,6 +6,7 @@ block content
script#data(type="application/json").
!{JSON.stringify({ projects: projects, tags: tags, notifications: notifications }).replace(/\//g, '\\/')}
script(type="text/javascript").
window.data = JSON.parse($("#data").text());
window.algolia = {
+28 -5
View File
@@ -213,6 +213,33 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate')
ng-click="delete()"
) #{translate("confirm")}
script(type="text/template", id="qq-project-uploader-template")
div.qq-uploader-selector
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
div.qq-upload-button-selector.btn.btn-primary.btn-lg
div #{translate('select_a_zip_file')}
span.or.btn-lg #{translate('or')}
span.drag-here.btn-lg #{translate('drag_a_zip_file')}
span.qq-drop-processing-selector
span #{translate('creating_project')}
span.qq-drop-processing-spinner-selector
ul.qq-upload-list-selector
li
div.qq-progress-bar-container-selector
div(
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
class="qq-progress-bar-selector qq-progress-bar"
)
span.qq-upload-file-selector.qq-upload-file
span.qq-upload-size-selector.qq-upload-size
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
script(type="text/ng-template", id="uploadProjectModalTemplate")
.modal-header
button.close(
@@ -224,15 +251,11 @@ script(type="text/ng-template", id="uploadProjectModalTemplate")
.modal-body(
fine-upload
endpoint="/project/new/upload"
waiting-for-response-text="Creating project..."
failed-upload-text="Upload failed. Is it a valid zip file?"
upload-button-text="Select a .zip file"
drag-area-text="drag .zip file"
template-id="qq-project-uploader-template"
multiple="false"
allowed-extensions="['zip']"
on-complete-callback="onComplete"
)
span #{translate("upload_a_zipped_project")}
.modal-footer
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
@@ -61,7 +61,10 @@ block content
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
p.pull-right
p
a(href="/user/subscription/billing-details/edit").btn.btn-info #{translate("update_your_billing_details")}
if billingDetailsLink
a(href=billingDetailsLink, target="_blank").btn.btn-info #{translate("update_your_billing_details")}
else
a(href=billingDetailsLink, disabled).btn.btn-info.btn-disabled #{translate("update_your_billing_details")}
| &nbsp;
a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
when "canceled"
@@ -1,27 +0,0 @@
extends ../layout
block scripts
script(src=buildJsPath('libs/recurly.min.js', {fingerprint:false}))
block content
.content.content-alt
.container
.row
.col-md-6.col-md-offset-3
.card
.page-header
h1.text-centered #{translate("update_your_billing_details")}
#billingDetailsForm #{translate("loading_billing_form")}...
script(type="text/javascript").
Recurly.config(!{recurlyConfig})
Recurly.buildBillingInfoUpdateForm({
target : "#billingDetailsForm",
successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails",
signature : "!{signature}",
accountCode : "#{user.id}"
});
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -e;
MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000"
$MOCHA "$@"
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling test/acceptance/coffee;
$COFFEE -o test/acceptance/js -c test/acceptance/coffee;
for dir in modules/*;
do
if [ -d $dir/test/acceptance ]; then
echo Compiling $dir/test/acceptance/coffee;
$COFFEE -o $dir/test/acceptance/js -c $dir/test/acceptance/coffee;
fi
done
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling app.coffee;
$COFFEE -c app.coffee;
echo Compiling app/coffee;
$COFFEE -o app/js -c app/coffee;
for dir in modules/*;
do
if [ -d $dir/app/coffee ]; then
echo Compiling $dir/app/coffee;
$COFFEE -o $dir/app/js -c $dir/app/coffee;
fi
if [ -e $dir/index.coffee ]; then
echo Compiling $dir/index.coffee;
$COFFEE -c $dir/index.coffee;
fi
done
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling test/unit/coffee;
$COFFEE -o test/unit/js -c test/unit/coffee;
for dir in modules/*;
do
if [ -d $dir/test/unit ]; then
echo Compiling $dir/test/unit/coffee;
$COFFEE -o $dir/test/unit/js -c $dir/test/unit/coffee;
fi
done
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python2
from os import listdir
from os.path import isfile, isdir, join
volumes = []
for module in listdir("modules/"):
if module[0] != '.':
if isfile(join("modules", module, 'index.coffee')):
volumes.append(join("modules", module, 'index.coffee'))
for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee', 'test/acceptance/config', 'test/acceptance/files']:
if isdir(join("modules", module, directory)):
volumes.append(join("modules", module, directory))
volumes_string = map(lambda vol: "- ./" + vol + ":/app/" + vol + ":ro", volumes)
volumes_string = "\n ".join(volumes_string)
with open("docker-shared.template.yml", "r") as f:
docker_shared_file = f.read()
docker_shared_file = docker_shared_file.replace("MODULE_VOLUMES", volumes_string)
with open("docker-shared.yml", "w") as f:
f.write(docker_shared_file)
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
set -e;
MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
$MOCHA "$@" test/unit/js
for dir in modules/*;
do
if [ -d $dir/test/unit/js ]; then
$MOCHA "$@" $dir/test/unit/js
fi
done
+8 -7
View File
@@ -35,12 +35,12 @@ module.exports = settings =
# Databases
# ---------
mongo:
url : 'mongodb://127.0.0.1/sharelatex'
url : process.env['MONGO_URL'] || "mongodb://127.0.0.1/sharelatex"
redis:
web:
host: "localhost"
port: "6379"
host: process.env['REDIS_HOST'] || "localhost"
port: process.env['REDIS_PORT'] || "6379"
password: ""
# websessions:
@@ -74,8 +74,8 @@ module.exports = settings =
# ]
api:
host: "localhost"
port: "6379"
host: process.env['REDIS_HOST'] || "localhost"
port: process.env['REDIS_PORT'] || "6379"
password: ""
# Service locations
@@ -87,6 +87,7 @@ module.exports = settings =
internal:
web:
port: webPort = 3000
host: process.env['LISTEN_ADDRESS'] or 'localhost'
documentupdater:
port: docUpdaterPort = 3003
@@ -99,7 +100,7 @@ module.exports = settings =
user: httpAuthUser
pass: httpAuthPass
documentupdater:
url : "http://localhost:#{docUpdaterPort}"
url : "http://#{process.env['DOCUPDATER_HOST'] or 'localhost'}:#{docUpdaterPort}"
thirdPartyDataStore:
url : "http://localhost:3002"
emptyProjectFlushDelayMiliseconds: 5 * seconds
@@ -113,7 +114,7 @@ module.exports = settings =
enabled: false
url : "http://localhost:3054"
docstore:
url : "http://localhost:3016"
url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"
pubUrl: "http://localhost:3016"
chat:
url: "http://localhost:3010"
+36
View File
@@ -0,0 +1,36 @@
version: "2"
volumes:
node_modules:
services:
npm:
extends:
file: docker-shared.yml
service: app
command: npm install
test_unit:
extends:
file: docker-shared.yml
service: app
command: npm run test:unit
test_acceptance:
extends:
file: docker-shared.yml
service: app
environment:
REDIS_HOST: redis
MONGO_URL: "mongodb://mongo/sharelatex"
SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true'
depends_on:
- redis
- mongo
command: npm run start
redis:
image: redis
mongo:
image: mongo:3.4.6
+30
View File
@@ -0,0 +1,30 @@
version: "2"
# We mount all the directories explicitly so that we are only mounting
# the coffee directories, so that the compiled js is only written inside
# the container, and not back to the local filesystem, where it would be
# root owned, and conflict with working outside of the container.
services:
app:
image: node:6.9.5
volumes:
- ./package.json:/app/package.json
- ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json
- node_modules:/app/node_modules
- ./bin:/app/bin
# Copying the whole public dir is fine for now, and needed for
# some unit tests to pass, but we will want to isolate the coffee
# and vendor js files, so that the compiled js files are not written
# back to the local filesystem.
- ./public:/app/public
- ./app.coffee:/app/app.coffee:ro
- ./app/coffee:/app/app/coffee:ro
- ./app/templates:/app/app/templates:ro
- ./app/views:/app/app/views:ro
- ./config:/app/config
- ./test/unit/coffee:/app/test/unit/coffee:ro
- ./test/acceptance/coffee:/app/test/acceptance/coffee:ro
- ./test/smoke/coffee:/app/test/smoke/coffee:ro
MODULE_VOLUMES
working_dir: /app
+6 -12
View File
@@ -1,12 +1,6 @@
*/app/js
*/test/unit/js
*/index.js
ldap
admin-panel
groovehq
launchpad
learn-wiki
references-search
sharelatex-saml
templates
tpr-webmodule
# Ignore all modules except for a whitelist
*
!dropbox
!github-sync
!public-registration
!.gitignore
+12
View File
@@ -9,6 +9,17 @@
"directories": {
"public": "./public"
},
"scripts": {
"test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done",
"test:acceptance:run": "bin/acceptance_test $@",
"test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@",
"test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js",
"test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@",
"compile:unit_tests": "bin/compile_unit_tests",
"compile:acceptance_tests": "bin/compile_acceptance_tests",
"compile:app": "bin/compile_app",
"start": "npm -q run compile:app && node app.js"
},
"dependencies": {
"archiver": "0.9.0",
"async": "0.6.2",
@@ -98,6 +109,7 @@
"grunt-postcss": "^0.8.0",
"grunt-sed": "^0.1.1",
"grunt-shell": "^2.1.0",
"mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"sandboxed-module": "0.2.0",
"sinon": "^1.17.0",
"timekeeper": "",
@@ -1,16 +1,13 @@
define [
"base"
], (App) ->
"fineuploader"
], (App, qq) ->
App.directive 'fineUpload', ($timeout) ->
return {
scope: {
multiple: "="
endpoint: "@"
waitingForResponseText: "@"
failedUploadText: "@"
uploadButtonText: "@"
dragAreaText: "@"
hintText: "@"
templateId: "@"
allowedExtensions: "="
onCompleteCallback: "="
onUploadCallback: "="
@@ -25,17 +22,12 @@ define [
link: (scope, element, attrs) ->
multiple = scope.multiple or false
endpoint = scope.endpoint
templateId = scope.templateId
if scope.allowedExtensions?
validation =
validation =
allowedExtensions: scope.allowedExtensions
else
validation = {}
text =
waitingForResponse: scope.waitingForResponseText or "Processing..."
failUpload: scope.failedUploadText or "Failed :("
uploadButton: scope.uploadButtonText or "Upload"
dragAreaText = scope.dragAreaText or "drag here"
hintText = scope.hintText or ""
maxConnections = scope.maxConnections or 1
onComplete = scope.onCompleteCallback or () ->
onUpload = scope.onUploadCallback or () ->
@@ -69,21 +61,8 @@ define [
onError: onError
onSubmit: onSubmit
onCancel: onCancel
text: text
template: """
<div class="qq-uploader">
<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>
<div class="qq-upload-button btn btn-primary btn-lg">
<div>{uploadButtonText}</div>
</div>
<span class="or btn-lg"> or </span>
<span class="drag-here btn-lg">#{dragAreaText}</span>
<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>
<div class="small">#{hintText}</div>
<ul class="qq-upload-list"></ul>
</div>
"""
template: templateId
window.q = q
scope.control?.q = q
return q
}
}
+9 -9
View File
@@ -9,7 +9,7 @@ define [
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
"ide/references/ReferencesManager"
"ide/labels/LabelsManager"
"ide/metadata/MetadataManager"
"ide/review-panel/ReviewPanelManager"
"ide/SafariScrollPatcher"
"ide/AutoCompileOnboardingController",
@@ -47,12 +47,12 @@ define [
PdfManager
BinaryFilesManager
ReferencesManager
LabelsManager
MetadataManager
ReviewPanelManager
SafariScrollPatcher
) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, labels) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) ->
# Don't freak out if we're already in an apply callback
$scope.$originalApply = $scope.$apply
$scope.$apply = (fn = () ->) ->
@@ -72,10 +72,10 @@ define [
view: "editor"
chatOpen: false
pdfLayout: 'sideBySide'
pdfHidden: false,
pdfWidth: 0,
reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}"),
miniReviewPanelVisible: false,
pdfHidden: false
pdfWidth: 0
reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}")
miniReviewPanelVisible: false
}
$scope.onboarding = {
autoCompile: if window.showAutoCompileOnboarding then 'unseen' else 'dismissed'
@@ -138,7 +138,7 @@ define [
ide.pdfManager = new PdfManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
ide.labelsManager = new LabelsManager(ide, $scope, labels)
ide.metadataManager = new MetadataManager(ide, $scope, metadata)
inited = false
$scope.$on "project:joined", () ->
@@ -153,7 +153,7 @@ define [
$timeout(
() ->
if $scope.permissions.write
ide.labelsManager.loadProjectLabelsFromServer()
ide.metadataManager.loadProjectMetaFromServer()
_labelsInitialLoadDone = true
, 200
)
@@ -9,11 +9,14 @@ define [
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
"ide/editor/directives/aceEditor/metadata/MetadataManager"
"ide/editor/directives/aceEditor/labels/LabelsManager"
"ide/labels/services/labels"
"ide/metadata/services/metadata"
"ide/graphics/services/graphics"
"ide/preamble/services/preamble"
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) ->
"ide/files/services/files"
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager, LabelsManager) ->
EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist')
@@ -35,7 +38,7 @@ define [
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, graphics, preamble, $http, $q) ->
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, metadata, graphics, preamble, files, $http, $q) ->
monkeyPatchSearch($rootScope, $compile)
return {
@@ -103,7 +106,8 @@ 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, graphics, preamble)
metadataManager = new MetadataManager(scope, editor, element, metadata)
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, labelsManager, graphics, preamble, files)
# Prevert Ctrl|Cmd-S from triggering save dialog
@@ -115,16 +119,16 @@ define [
editor.commands.removeCommand "transposeletters"
editor.commands.removeCommand "showSettingsMenu"
editor.commands.removeCommand "foldall"
# For European keyboards, the / is above 7 so needs Shift pressing.
# This comes through as Command-Shift-/ on OS X, which is mapped to
# This comes through as Command-Shift-/ on OS X, which is mapped to
# toggleBlockComment.
# This doesn't do anything for LaTeX, so remap this to togglecomment to
# work for European keyboards as normal.
# On Windows, the key combo comes as Ctrl-Shift-7.
editor.commands.removeCommand "toggleBlockComment"
editor.commands.removeCommand "togglecomment"
editor.commands.addCommand {
name: "togglecomment",
bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" },
@@ -140,7 +144,7 @@ define [
exec: (editor) ->
ace.require("ace/ext/searchbox").Search(editor, true)
readOnly: true
# Bold text on CMD+B
editor.commands.addCommand
name: "bold",
@@ -154,7 +158,7 @@ define [
text = editor.getCopyText()
editor.insert("\\textbf{" + text + "}")
readOnly: false
# Italicise text on CMD+I
editor.commands.addCommand
name: "italics",
@@ -171,7 +175,7 @@ define [
scope.$watch "onCtrlEnter", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "compile",
bindKey: win: "Ctrl-Enter", mac: "Command-Enter"
exec: (editor) =>
@@ -180,7 +184,7 @@ define [
scope.$watch "onCtrlJ", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "toggle-review-panel",
bindKey: win: "Ctrl-J", mac: "Command-J"
exec: (editor) =>
@@ -189,7 +193,7 @@ define [
scope.$watch "onCtrlShiftC", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "add-new-comment",
bindKey: win: "Ctrl-Shift-C", mac: "Command-Shift-C"
exec: (editor) =>
@@ -198,7 +202,7 @@ define [
scope.$watch "onCtrlShiftA", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "toggle-track-changes",
bindKey: win: "Ctrl-Shift-A", mac: "Command-Shift-A"
exec: (editor) =>
@@ -302,7 +306,7 @@ define [
if updateCount == 100
event_tracking.send 'editor-interaction', 'multi-doc-update'
scope.$emit "#{scope.name}:change"
onScroll = (scrollTop) ->
return if !scope.eventsBridge?
height = editor.renderer.layerConfig.maxHeight
@@ -311,7 +315,7 @@ define [
onScrollbarVisibilityChanged = (event, vRenderer) ->
return if !scope.eventsBridge?
scope.eventsBridge.emit "aceScrollbarVisibilityChanged", vRenderer.scrollBarV.isVisible, vRenderer.scrollBarV.width
if scope.eventsBridge?
editor.renderer.on "scrollbarVisibilityChanged", onScrollbarVisibilityChanged
@@ -404,14 +408,14 @@ define [
session = editor.getSession()
session.off "changeScrollTop"
doc = session.getDocument()
doc.off "change", onChange
editor.renderer.on "changeCharacterSize", () ->
scope.$apply () ->
scope.rendererData.lineHeight = editor.renderer.lineHeight
scope.$watch "rendererData", (rendererData) ->
if rendererData?
rendererData.lineHeight = editor.renderer.lineHeight
@@ -1,16 +1,16 @@
define [
"ide/editor/directives/aceEditor/auto-complete/CommandManager"
"ide/editor/directives/aceEditor/auto-complete/EnvironmentManager"
"ide/editor/directives/aceEditor/auto-complete/PackageManager"
"ide/editor/directives/aceEditor/auto-complete/Helpers"
"ace/ace"
"ace/ext-language_tools"
], (CommandManager, EnvironmentManager, Helpers) ->
], (CommandManager, EnvironmentManager, PackageManager, Helpers) ->
Range = ace.require("ace/range").Range
aceSnippetManager = ace.require('ace/snippets').snippetManager
class AutoCompleteManager
constructor: (@$scope, @editor, @element, @labelsManager, @graphics, @preamble) ->
@suggestionManager = new CommandManager()
constructor: (@$scope, @editor, @element, @metadataManager, @labelsManager, @graphics, @preamble, @files) ->
@monkeyPatchAutocomplete()
@@ -34,10 +34,15 @@ define [
enableLiveAutocompletion: false
})
commandCompleter = new CommandManager(@metadataManager)
SnippetCompleter = new EnvironmentManager()
PackageCompleter = new PackageManager()
Graphics = @graphics
Preamble = @preamble
Files = @files
GraphicsCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
@@ -63,7 +68,28 @@ define [
}
callback null, result
labelsManager = @labelsManager
metadataManager = @metadataManager
FilesCompleter =
getCompletions: (editor, session, pos, prefix, callback) =>
context = Helpers.getContext(editor, pos)
{lineUpToCursor, commandFragment, lineBeyondCursor, needsClosingBrace} = context
if commandFragment
match = commandFragment.match(/^\\(input|include){(\w*)/)
if match
commandName = match[1]
currentArg = match[2]
result = []
for file in Files.getTeXFiles()
if file.id != @$scope.docId
path = file.path
result.push {
caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
meta: "file",
score: 50
}
callback null, result
LabelsCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
@@ -74,13 +100,14 @@ define [
commandName = refMatch[1]
currentArg = refMatch[2]
result = []
result.push {
caption: "\\#{commandName}{}",
snippet: "\\#{commandName}{}",
meta: "cross-reference",
score: 60
}
for label in labelsManager.getAllLabels()
if commandName != 'ref' # ref is in top 100 commands
result.push {
caption: "\\#{commandName}{}",
snippet: "\\#{commandName}{}",
meta: "cross-reference",
score: 60
}
for label in metadataManager.getAllLabels()
result.push {
caption: "\\#{commandName}{#{label}#{if needsClosingBrace then '}' else ''}",
value: "\\#{commandName}{#{label}#{if needsClosingBrace then '}' else ''}",
@@ -126,11 +153,13 @@ define [
callback null, result
@editor.completers = [
@suggestionManager,
SnippetCompleter,
ReferencesCompleter,
LabelsCompleter,
commandCompleter
SnippetCompleter
PackageCompleter
ReferencesCompleter
LabelsCompleter
GraphicsCompleter
FilesCompleter
]
disable: () ->
@@ -152,8 +181,6 @@ define [
if lastTwoChars.match(/^\\[^a-zA-Z]$/)
@editor?.completer?.detach?()
return
if commandName in ['begin', 'end']
return
# Check that this change was made by us, not a collaborator
# (Cursor is still one place behind)
# NOTE: this is also the case when a user backspaces over a highlighted region
@@ -1,81 +1,8 @@
define [], () ->
noArgumentCommands = [
'item', 'hline', 'lipsum', 'centering', 'noindent', 'textwidth', 'draw',
'maketitle', 'newpage', 'verb', 'bibliography', 'hfill', 'par',
'in', 'sum', 'cdot', 'ldots', 'linewidth', 'left', 'right', 'today',
'clearpage', 'newline', 'endinput', 'tableofcontents', 'vfill',
'bigskip', 'fill', 'cleardoublepage', 'infty', 'leq', 'geq', 'times',
'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'
]
singleArgumentCommands = [
'chapter', 'usepackage', 'section', 'label', 'textbf', 'subsection',
'vspace', 'cite', 'textit', 'documentclass', 'includegraphics', 'input',
'emph','caption', 'ref', 'title', 'author', 'texttt', 'include',
'hspace', 'bibitem', 'url', 'large', 'subsubsection', 'textsc', 'date',
'footnote', 'small', 'thanks', 'underline', 'graphicspath', 'pageref',
'section*', 'subsection*', 'subsubsection*', 'sqrt', 'text',
'normalsize', 'footnotesize', 'Large', 'paragraph', 'pagestyle',
'thispagestyle', 'bibliographystyle', 'hat'
]
doubleArgumentCommands = [
'newcommand', 'frac', 'dfrac', 'renewcommand', 'setlength', 'href',
'newtheorem'
]
tripleArgumentCommands = [
'addcontentsline', 'newacronym', 'multicolumn'
]
special = ['LaTeX', 'TeX']
define [
"./top_hundred_snippets"
], (topHundred) ->
rawCommands = [].concat(
noArgumentCommands,
singleArgumentCommands,
doubleArgumentCommands,
tripleArgumentCommands,
special
)
noArgumentCommands = for cmd in noArgumentCommands
{
caption: "\\#{cmd}"
snippet: "\\#{cmd}"
meta: "cmd"
}
singleArgumentCommands = for cmd in singleArgumentCommands
{
caption: "\\#{cmd}{}"
snippet: "\\#{cmd}{$1}"
meta: "cmd"
}
doubleArgumentCommands = for cmd in doubleArgumentCommands
{
caption: "\\#{cmd}{}{}"
snippet: "\\#{cmd}{$1}{$2}"
meta: "cmd"
}
tripleArgumentCommands = for cmd in tripleArgumentCommands
{
caption: "\\#{cmd}{}{}{}"
snippet: "\\#{cmd}{$1}{$2}{$3}"
meta: "cmd"
}
special = for cmd in special
{
caption: "\\#{cmd}{}"
snippet: "\\#{cmd}{}"
meta: "cmd"
}
staticCommands = [].concat(
noArgumentCommands,
singleArgumentCommands,
doubleArgumentCommands,
tripleArgumentCommands,
special
)
commandNames = (snippet.caption.match(/\w+/)[0] for snippet in topHundred)
class Parser
constructor: (@doc, @prefix) ->
@@ -129,10 +56,10 @@ define [], () ->
return realCommands
# Ignore single letter commands since auto complete is moot then.
commandRegex: /\\([a-zA-Z][a-zA-Z]+)/
commandRegex: /\\([a-zA-Z]{2,})/
nextCommand: () ->
i = @doc.search(@commandRegex)
i = @doc.search @commandRegex
if i == -1
return false
else
@@ -166,13 +93,21 @@ define [], () ->
return false
class CommandManager
constructor: (@metadataManager) ->
getCompletions: (editor, session, pos, prefix, callback) ->
packages = @metadataManager.getAllPackages()
packageCommands = []
for pkg, snippets of packages
for snippet in snippets
packageCommands.push snippet
doc = session.getValue()
parser = new Parser(doc, prefix)
commands = parser.parse()
completions = []
for command in commands
if command[0] not in rawCommands
if command[0] not in commandNames
caption = "\\#{command[0]}"
score = if caption == prefix then 99 else 50
snippet = caption
@@ -191,9 +126,9 @@ define [], () ->
meta: "cmd"
score: score
}
completions = completions.concat staticCommands
completions = completions.concat topHundred, packageCommands
callback(null, completions)
callback null, completions
loadCommandsFromDoc: (doc) ->
parser = new Parser(doc)
@@ -0,0 +1,41 @@
define () ->
packages = [
'inputenc', 'graphicx', 'amsmath', 'geometry', 'amssymb', 'hyperref',
'babel', 'color', 'xcolor', 'url', 'natbib', 'fontenc', 'fancyhdr',
'amsfonts', 'booktabs', 'amsthm', 'float', 'tikz', 'caption',
'setspace', 'multirow', 'array', 'multicol', 'titlesec', 'enumitem',
'ifthen', 'listings', 'blindtext', 'subcaption', 'times', 'bm',
'subfigure', 'algorithm', 'fontspec', 'biblatex', 'tabularx',
'microtype', 'etoolbox', 'parskip', 'calc', 'verbatim', 'mathtools',
'epsfig', 'wrapfig', 'lipsum', 'cite', 'textcomp', 'longtable',
'textpos', 'algpseudocode', 'enumerate', 'subfig', 'pdfpages',
'epstopdf', 'latexsym', 'lmodern', 'pifont', 'ragged2e', 'rotating',
'dcolumn', 'xltxtra', 'marvosym', 'indentfirst', 'xspace', 'csquotes',
'xparse', 'changepage', 'soul', 'xunicode', 'comment', 'mathrsfs',
'tocbibind', 'lastpage', 'algorithm2e', 'pgfplots', 'lineno',
'graphics', 'algorithmic', 'fullpage', 'mathptmx', 'todonotes',
'ulem', 'tweaklist', 'moderncvstyleclassic', 'collection',
'moderncvcompatibility', 'gensymb', 'helvet', 'siunitx', 'adjustbox',
'placeins', 'colortbl', 'appendix', 'makeidx', 'supertabular', 'ifpdf',
'framed', 'aliascnt', 'layaureo', 'authblk'
]
packageSnippets = for pkg in packages
{
caption: "\\usepackage{#{pkg}}"
snippet: "\\usepackage{#{pkg}}"
meta: "pkg"
}
packageSnippets.push {
caption: "\\usepackage{}"
snippet: "\\usepackage{$1}"
meta: "pkg"
score: 70
}
class PackageManager
getCompletions: (editor, session, pos, prefix, callback) ->
callback null, packageSnippets
return PackageManager
@@ -0,0 +1,691 @@
define -> [{
"caption": "\\begin{}",
"snippet": "\\begin{$1}",
"meta": "cmd",
"score": 7.849662248028187
}, {
"caption": "\\begin{}[]",
"snippet": "\\begin{$1}[$2]",
"meta": "cmd",
"score": 7.849662248028187
}, {
"caption": "\\begin{}{}",
"snippet": "\\begin{$1}{$2}",
"meta": "cmd",
"score": 7.849662248028187
}, {
"caption": "\\end{}",
"snippet": "\\end{$1}",
"meta": "cmd",
"score": 7.847906405228455
}, {
"caption": "\\usepackage{}",
"snippet": "\\usepackage{$1}",
"meta": "cmd",
"score": 5.427890758130527
}, {
"caption": "\\usepackage[]{}",
"snippet": "\\usepackage[$1]{$2}",
"meta": "cmd",
"score": 5.427890758130527
}, {
"caption": "\\item",
"snippet": "\\item",
"meta": "cmd",
"score": 3.800886892251021
}, {
"caption": "\\item[]",
"snippet": "\\item[$1]",
"meta": "cmd",
"score": 3.800886892251021
}, {
"caption": "\\section{}",
"snippet": "\\section{$1}",
"meta": "cmd",
"score": 3.0952612541683835
}, {
"caption": "\\textbf{}",
"snippet": "\\textbf{$1}",
"meta": "cmd",
"score": 2.627755982816738
}, {
"caption": "\\cite{}",
"snippet": "\\cite{$1}",
"meta": "cmd",
"score": 2.341195220791228
}, {
"caption": "\\label{}",
"snippet": "\\label{$1}",
"meta": "cmd",
"score": 1.897791904799601
}, {
"caption": "\\textit{}",
"snippet": "\\textit{$1}",
"meta": "cmd",
"score": 1.6842996195493385
}, {
"caption": "\\includegraphics[]{}",
"snippet": "\\includegraphics[$1]{$2}",
"meta": "cmd",
"score": 1.4595731795525781
}, {
"caption": "\\documentclass[]{}",
"snippet": "\\documentclass[$1]{$2}",
"meta": "cmd",
"score": 1.4425339817971206
}, {
"caption": "\\documentclass{}",
"snippet": "\\documentclass{$1}",
"meta": "cmd",
"score": 1.4425339817971206
}, {
"caption": "\\frac{}{}",
"snippet": "\\frac{$1}{$2}",
"meta": "cmd",
"score": 1.4341091141105058
}, {
"caption": "\\subsection{}",
"snippet": "\\subsection{$1}",
"meta": "cmd",
"score": 1.3890912739512353
}, {
"caption": "\\hline",
"snippet": "\\hline",
"meta": "cmd",
"score": 1.3209538327406387
}, {
"caption": "\\caption{}",
"snippet": "\\caption{$1}",
"meta": "cmd",
"score": 1.2569477427490174
}, {
"caption": "\\centering",
"snippet": "\\centering",
"meta": "cmd",
"score": 1.1642881814937829
}, {
"caption": "\\vspace{}",
"snippet": "\\vspace{$1}",
"meta": "cmd",
"score": 0.9533807826673939
}, {
"caption": "\\title{}",
"snippet": "\\title{$1}",
"meta": "cmd",
"score": 0.9202908262245683
}, {
"caption": "\\author{}",
"snippet": "\\author{$1}",
"meta": "cmd",
"score": 0.8973590434087177
}, {
"caption": "\\author[]{}",
"snippet": "\\author[$1]{$2}",
"meta": "cmd",
"score": 0.8973590434087177
}, {
"caption": "\\maketitle",
"snippet": "\\maketitle",
"meta": "cmd",
"score": 0.7504160124360846
}, {
"caption": "\\textwidth",
"snippet": "\\textwidth",
"meta": "cmd",
"score": 0.7355328080889112
}, {
"caption": "\\newcommand{}{}",
"snippet": "\\newcommand{$1}{$2}",
"meta": "cmd",
"score": 0.7264891987129375
}, {
"caption": "\\newcommand{}[]{}",
"snippet": "\\newcommand{$1}[$2]{$3}",
"meta": "cmd",
"score": 0.7264891987129375
}, {
"caption": "\\date{}",
"snippet": "\\date{$1}",
"meta": "cmd",
"score": 0.7225518453076786
}, {
"caption": "\\emph{}",
"snippet": "\\emph{$1}",
"meta": "cmd",
"score": 0.7060308784832261
}, {
"caption": "\\textsc{}",
"snippet": "\\textsc{$1}",
"meta": "cmd",
"score": 0.6926466355384758
}, {
"caption": "\\multicolumn{}{}{}",
"snippet": "\\multicolumn{$1}{$2}{$3}",
"meta": "cmd",
"score": 0.5473606021405326
}, {
"caption": "\\input{}",
"snippet": "\\input{$1}",
"meta": "cmd",
"score": 0.4966021927742672
}, {
"caption": "\\alpha",
"snippet": "\\alpha",
"meta": "cmd",
"score": 0.49520006391384913
}, {
"caption": "\\in",
"snippet": "\\in",
"meta": "cmd",
"score": 0.4716039670146658
}, {
"caption": "\\mathbf{}",
"snippet": "\\mathbf{$1}",
"meta": "cmd",
"score": 0.4682018419466319
}, {
"caption": "\\right",
"snippet": "\\right",
"meta": "cmd",
"score": 0.4299239459457309
}, {
"caption": "\\left",
"snippet": "\\left",
"meta": "cmd",
"score": 0.42937815279867964
}, {
"caption": "\\left[]",
"snippet": "\\left[$1]",
"meta": "cmd",
"score": 0.42937815279867964
}, {
"caption": "\\sum",
"snippet": "\\sum",
"meta": "cmd",
"score": 0.42607994509619934
}, {
"caption": "\\noindent",
"snippet": "\\noindent",
"meta": "cmd",
"score": 0.42355747798114207
}, {
"caption": "\\chapter{}",
"snippet": "\\chapter{$1}",
"meta": "cmd",
"score": 0.422097569591803
}, {
"caption": "\\par",
"snippet": "\\par",
"meta": "cmd",
"score": 0.413853376001159
}, {
"caption": "\\lambda",
"snippet": "\\lambda",
"meta": "cmd",
"score": 0.39389600578684125
}, {
"caption": "\\subsubsection{}",
"snippet": "\\subsubsection{$1}",
"meta": "cmd",
"score": 0.3727781330132016
}, {
"caption": "\\bibitem{}",
"snippet": "\\bibitem{$1}",
"meta": "cmd",
"score": 0.3689547570562042
}, {
"caption": "\\bibitem[]{}",
"snippet": "\\bibitem[$1]{$2}",
"meta": "cmd",
"score": 0.3689547570562042
}, {
"caption": "\\text{}",
"snippet": "\\text{$1}",
"meta": "cmd",
"score": 0.3608680734736821
}, {
"caption": "\\setlength{}{}",
"snippet": "\\setlength{$1}{$2}",
"meta": "cmd",
"score": 0.354445763583904
}, {
"caption": "\\setlength",
"snippet": "\\setlength",
"meta": "cmd",
"score": 0.354445763583904
}, {
"caption": "\\mathcal{}",
"snippet": "\\mathcal{$1}",
"meta": "cmd",
"score": 0.35084018920966636
}, {
"caption": "\\newline",
"snippet": "\\newline",
"meta": "cmd",
"score": 0.3311721696201715
}, {
"caption": "\\newpage",
"snippet": "\\newpage",
"meta": "cmd",
"score": 0.3277033727934986
}, {
"caption": "\\renewcommand{}{}",
"snippet": "\\renewcommand{$1}{$2}",
"meta": "cmd",
"score": 0.3267437011085663
}, {
"caption": "\\renewcommand",
"snippet": "\\renewcommand",
"meta": "cmd",
"score": 0.3267437011085663
}, {
"caption": "\\theta",
"snippet": "\\theta",
"meta": "cmd",
"score": 0.3210417159232142
}, {
"caption": "\\hspace{}",
"snippet": "\\hspace{$1}",
"meta": "cmd",
"score": 0.3147206476372336
}, {
"caption": "\\beta",
"snippet": "\\beta",
"meta": "cmd",
"score": 0.3061799530337638
}, {
"caption": "\\texttt{}",
"snippet": "\\texttt{$1}",
"meta": "cmd",
"score": 0.3019066753744355
}, {
"caption": "\\times",
"snippet": "\\times",
"meta": "cmd",
"score": 0.2957960629411553
}, {
"caption": "\\citep{}",
"snippet": "\\citep{$1}",
"meta": "cmd",
"score": 0.2941882834697057
}, {
"caption": "\\color[]{}",
"snippet": "\\color[$1]{$2}",
"meta": "cmd",
"score": 0.2864294797053033
}, {
"caption": "\\color{}",
"snippet": "\\color{$1}",
"meta": "cmd",
"score": 0.2864294797053033
}, {
"caption": "\\mu",
"snippet": "\\mu",
"meta": "cmd",
"score": 0.27635652476799255
}, {
"caption": "\\bibliography{}",
"snippet": "\\bibliography{$1}",
"meta": "cmd",
"score": 0.2659628337907604
}, {
"caption": "\\linewidth",
"snippet": "\\linewidth",
"meta": "cmd",
"score": 0.2639498312518439
}, {
"caption": "\\delta",
"snippet": "\\delta",
"meta": "cmd",
"score": 0.2620578600722735
}, {
"caption": "\\sigma",
"snippet": "\\sigma",
"meta": "cmd",
"score": 0.25940147926344487
}, {
"caption": "\\pi",
"snippet": "\\pi",
"meta": "cmd",
"score": 0.25920934567729714
}, {
"caption": "\\hat{}",
"snippet": "\\hat{$1}",
"meta": "cmd",
"score": 0.25264309033778715
}, {
"caption": "\\hat",
"snippet": "\\hat",
"meta": "cmd",
"score": 0.25264309033778715
}, {
"caption": "\\bibliographystyle{}",
"snippet": "\\bibliographystyle{$1}",
"meta": "cmd",
"score": 0.25122317941387773
}, {
"caption": "\\small",
"snippet": "\\small",
"meta": "cmd",
"score": 0.2447632045426295
}, {
"caption": "\\small{}",
"snippet": "\\small{$1}",
"meta": "cmd",
"score": 0.2447632045426295
}, {
"caption": "\\LaTeX",
"snippet": "\\LaTeX",
"meta": "cmd",
"score": 0.2334089308452787
}, {
"caption": "\\LaTeX{}",
"snippet": "\\LaTeX{$1}",
"meta": "cmd",
"score": 0.2334089308452787
}, {
"caption": "\\cdot",
"snippet": "\\cdot",
"meta": "cmd",
"score": 0.23029085545522762
}, {
"caption": "\\footnote{}",
"snippet": "\\footnote{$1}",
"meta": "cmd",
"score": 0.2253056071787701
}, {
"caption": "\\newtheorem{}[]{}",
"snippet": "\\newtheorem{$1}[$2]{$3}",
"meta": "cmd",
"score": 0.215689795055434
}, {
"caption": "\\newtheorem{}{}",
"snippet": "\\newtheorem{$1}{$2}",
"meta": "cmd",
"score": 0.215689795055434
}, {
"caption": "\\newtheorem{}{}[]",
"snippet": "\\newtheorem{$1}{$2}[$3]",
"meta": "cmd",
"score": 0.215689795055434
}, {
"caption": "\\Delta",
"snippet": "\\Delta",
"meta": "cmd",
"score": 0.21386475063892618
}, {
"caption": "\\tau",
"snippet": "\\tau",
"meta": "cmd",
"score": 0.21236188205859796
}, {
"caption": "\\hfill",
"snippet": "\\hfill",
"meta": "cmd",
"score": 0.2058248088519886
}, {
"caption": "\\leq",
"snippet": "\\leq",
"meta": "cmd",
"score": 0.20498894440637172
}, {
"caption": "\\footnotesize",
"snippet": "\\footnotesize",
"meta": "cmd",
"score": 0.2038592081252624
}, {
"caption": "\\footnotesize{}",
"snippet": "\\footnotesize{$1}",
"meta": "cmd",
"score": 0.2038592081252624
}, {
"caption": "\\large",
"snippet": "\\large",
"meta": "cmd",
"score": 0.20377416734108866
}, {
"caption": "\\large{}",
"snippet": "\\large{$1}",
"meta": "cmd",
"score": 0.20377416734108866
}, {
"caption": "\\sqrt{}",
"snippet": "\\sqrt{$1}",
"meta": "cmd",
"score": 0.20240160977404634
}, {
"caption": "\\epsilon",
"snippet": "\\epsilon",
"meta": "cmd",
"score": 0.2005136761359043
}, {
"caption": "\\Large",
"snippet": "\\Large",
"meta": "cmd",
"score": 0.1987771081149759
}, {
"caption": "\\Large{}",
"snippet": "\\Large{$1}",
"meta": "cmd",
"score": 0.1987771081149759
}, {
"caption": "\\cvitem{}{}",
"snippet": "\\cvitem{$1}{$2}",
"meta": "cmd",
"score": 0.19605476980016281
}, {
"caption": "\\rho",
"snippet": "\\rho",
"meta": "cmd",
"score": 0.1959287380541684
}, {
"caption": "\\omega",
"snippet": "\\omega",
"meta": "cmd",
"score": 0.19326783415115262
}, {
"caption": "\\mathrm{}",
"snippet": "\\mathrm{$1}",
"meta": "cmd",
"score": 0.19117752976172653
}, {
"caption": "\\boldsymbol{}",
"snippet": "\\boldsymbol{$1}",
"meta": "cmd",
"score": 0.18137737738638837
}, {
"caption": "\\boldsymbol",
"snippet": "\\boldsymbol",
"meta": "cmd",
"score": 0.18137737738638837
}, {
"caption": "\\gamma",
"snippet": "\\gamma",
"meta": "cmd",
"score": 0.17940276535431304
}, {
"caption": "\\clearpage",
"snippet": "\\clearpage",
"meta": "cmd",
"score": 0.1789117552185788
}, {
"caption": "\\infty",
"snippet": "\\infty",
"meta": "cmd",
"score": 0.17837290019711305
}, {
"caption": "\\phi",
"snippet": "\\phi",
"meta": "cmd",
"score": 0.17405809173097808
}, {
"caption": "\\partial",
"snippet": "\\partial",
"meta": "cmd",
"score": 0.17168102367966637
}, {
"caption": "\\include{}",
"snippet": "\\include{$1}",
"meta": "cmd",
"score": 0.1547080054979312
}, {
"caption": "\\address{}",
"snippet": "\\address{$1}",
"meta": "cmd",
"score": 0.1525055392611109
}, {
"caption": "\\address[]{}",
"snippet": "\\address[$1]{$2}",
"meta": "cmd",
"score": 0.1525055392611109
}, {
"caption": "\\address{}{}{}",
"snippet": "\\address{$1}{$2}{$3}",
"meta": "cmd",
"score": 0.1525055392611109
}, {
"caption": "\\quad",
"snippet": "\\quad",
"meta": "cmd",
"score": 0.15242755832392743
}, {
"caption": "\\email{}",
"snippet": "\\email{$1}",
"meta": "cmd",
"score": 0.1522294670109857
}, {
"caption": "\\paragraph{}",
"snippet": "\\paragraph{$1}",
"meta": "cmd",
"score": 0.152074250347974
}, {
"caption": "\\varepsilon",
"snippet": "\\varepsilon",
"meta": "cmd",
"score": 0.05411564201390573
}, {
"caption": "\\zeta",
"snippet": "\\zeta",
"meta": "cmd",
"score": 0.023330249803752954
}, {
"caption": "\\eta",
"snippet": "\\eta",
"meta": "cmd",
"score": 0.11088718379889091
}, {
"caption": "\\vartheta",
"snippet": "\\vartheta",
"meta": "cmd",
"score": 0.0025822992078068712
}, {
"caption": "\\iota",
"snippet": "\\iota",
"meta": "cmd",
"score": 0.0024774003791525486
}, {
"caption": "\\kappa",
"snippet": "\\kappa",
"meta": "cmd",
"score": 0.04887876299369008
}, {
"caption": "\\nu",
"snippet": "\\nu",
"meta": "cmd",
"score": 0.09206962821059342
}, {
"caption": "\\xi",
"snippet": "\\xi",
"meta": "cmd",
"score": 0.06496042899265699
}, {
"caption": "\\varpi",
"snippet": "\\varpi",
"meta": "cmd",
"score": 0.0007039358167790341
}, {
"caption": "\\varrho",
"snippet": "\\varrho",
"meta": "cmd",
"score": 0.0011279491613898612
}, {
"caption": "\\varsigma",
"snippet": "\\varsigma",
"meta": "cmd",
"score": 0.0010424880711234978
}, {
"caption": "\\varsigma{}",
"snippet": "\\varsigma{$1}",
"meta": "cmd",
"score": 0.0010424880711234978
}, {
"caption": "\\upsilon",
"snippet": "\\upsilon",
"meta": "cmd",
"score": 0.00420715572598688
}, {
"caption": "\\varphi",
"snippet": "\\varphi",
"meta": "cmd",
"score": 0.03351251516668212
}, {
"caption": "\\chi",
"snippet": "\\chi",
"meta": "cmd",
"score": 0.043373492287805675
}, {
"caption": "\\psi",
"snippet": "\\psi",
"meta": "cmd",
"score": 0.09994508706163642
}, {
"caption": "\\Gamma",
"snippet": "\\Gamma",
"meta": "cmd",
"score": 0.04801549269801977
}, {
"caption": "\\Theta",
"snippet": "\\Theta",
"meta": "cmd",
"score": 0.038090902146599444
}, {
"caption": "\\Lambda",
"snippet": "\\Lambda",
"meta": "cmd",
"score": 0.032206594305977686
}, {
"caption": "\\Xi",
"snippet": "\\Xi",
"meta": "cmd",
"score": 0.01060997225400494
}, {
"caption": "\\Pi",
"snippet": "\\Pi",
"meta": "cmd",
"score": 0.021264671817473237
}, {
"caption": "\\Sigma",
"snippet": "\\Sigma",
"meta": "cmd",
"score": 0.05769642802079917
}, {
"caption": "\\Upsilon",
"snippet": "\\Upsilon",
"meta": "cmd",
"score": 0.00032875192955749566
}, {
"caption": "\\Phi",
"snippet": "\\Phi",
"meta": "cmd",
"score": 0.0538724950042562
}, {
"caption": "\\Psi",
"snippet": "\\Psi",
"meta": "cmd",
"score": 0.03056589143021648
}, {
"caption": "\\Omega",
"snippet": "\\Omega",
"meta": "cmd",
"score": 0.09490387997853639
}]
@@ -0,0 +1,85 @@
define [
"ace/ace"
], () ->
Range = ace.require("ace/range").Range
getLastCommandFragment = (lineUpToCursor) ->
if m = lineUpToCursor.match(/(\\[^\\]+)$/)
return m[1]
else
return null
class MetadataManager
constructor: (@$scope, @editor, @element, @Metadata) ->
@debouncer = {} # DocId => Timeout
onChange = (change) =>
if change.remote
return
if change.action not in ['remove', 'insert']
return
cursorPosition = @editor.getCursorPosition()
end = change.end
range = new Range(end.row, 0, end.row, end.column)
lineUpToCursor = @editor.getSession().getTextRange range
if lineUpToCursor.trim() == '%' or lineUpToCursor.slice(0, 1) == '\\'
range = new Range(end.row, 0, end.row, end.column + 80)
lineUpToCursor = @editor.getSession().getTextRange range
commandFragment = getLastCommandFragment lineUpToCursor
linesContainPackage = _.any(
change.lines,
(line) -> line.match(/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
)
linesContainReqPackage = _.any(
change.lines,
(line) -> line.match(/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
)
linesContainLabel = _.any(
change.lines,
(line) -> line.match(/\\label{(.{0,80}?)}/)
)
linesContainMeta =
linesContainPackage or
linesContainLabel or
linesContainReqPackage
lastCommandFragmentIsLabel = commandFragment?.slice(0, 7) == '\\label{'
lastCommandFragmentIsPackage = commandFragment?.slice(0, 11) == '\\usepackage'
lastCommandFragmentIsReqPack = commandFragment?.slice(0, 15) == '\\RequirePackage'
lastCommandFragmentIsMeta =
lastCommandFragmentIsPackage or
lastCommandFragmentIsLabel or
lastCommandFragmentIsReqPack
if linesContainMeta or lastCommandFragmentIsMeta
@scheduleLoadCurrentDocMetaFromServer()
@editor.on "changeSession", (e) =>
e.oldSession.off "change", onChange
e.session.on "change", onChange
loadDocMetaFromServer: (docId) ->
@Metadata.loadDocMetaFromServer docId
scheduleLoadCurrentDocMetaFromServer: () ->
# De-bounce loading labels with a timeout
currentDocId = @$scope.docId
existingTimeout = @debouncer[currentDocId]
if existingTimeout?
clearTimeout(existingTimeout)
delete @debouncer[currentDocId]
@debouncer[currentDocId] = setTimeout(
() =>
@loadDocMetaFromServer currentDocId
delete @debouncer[currentDocId]
, 1000
, this
)
getAllLabels: () ->
@Metadata.getAllLabels()
getAllPackages: () ->
@Metadata.getAllPackages()
@@ -13,15 +13,15 @@ define [
@loadRootFolder()
@loadDeletedDocs()
@$scope.$emit "file-tree:initialized"
@$scope.$watch "rootFolder", (rootFolder) =>
if rootFolder?
@recalculateDocList()
@_bindToSocketEvents()
@$scope.multiSelectedCount = 0
$(document).on "click", =>
@clearMultiSelectedEntities()
$scope.$digest()
@@ -46,7 +46,7 @@ define [
type: "file"
}
@recalculateDocList()
@ide.socket.on "reciveNewFolder", (parent_folder_id, folder) =>
parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
@$scope.$apply () =>
@@ -85,25 +85,25 @@ define [
@ide.fileTreeManager.forEachEntity (entity) ->
entity.selected = false
entity.selected = true
toggleMultiSelectEntity: (entity) ->
entity.multiSelected = !entity.multiSelected
@$scope.multiSelectedCount = @multiSelectedCount()
multiSelectedCount: () ->
count = 0
@forEachEntity (entity) ->
if entity.multiSelected
count++
return count
getMultiSelectedEntities: () ->
entities = []
@forEachEntity (e) ->
if e.multiSelected
entities.push e
return entities
getMultiSelectedEntityChildNodes: () ->
entities = @getMultiSelectedEntities()
paths = {}
@@ -125,13 +125,13 @@ define [
if !prefixes[path]?
child_entities.push entity
return child_entities
clearMultiSelectedEntities: () ->
return if @$scope.multiSelectedCount == 0 # Be efficient, this is called a lot on 'click'
@forEachEntity (entity) ->
entity.multiSelected = false
@$scope.multiSelectedCount = 0
multiSelectSelectedEntity: () ->
@findSelectedEntity()?.multiSelected = true
@@ -140,7 +140,7 @@ define [
return false if !folder?
entity = @_findEntityByPathInFolder(folder, name)
return entity?
findSelectedEntity: () ->
selected = null
@forEachEntity (entity) ->
@@ -178,7 +178,7 @@ define [
parts = path.split("/")
name = parts.shift()
rest = parts.join("/")
if name == "."
return @_findEntityByPathInFolder(folder, rest)
@@ -268,7 +268,7 @@ define [
type: "doc"
deleted: true
}
recalculateDocList: () ->
@$scope.docs = []
@forEachEntity (entity, parentFolder, path) =>
@@ -287,7 +287,7 @@ define [
return -1
else
return 1
getEntityPath: (entity) ->
@_getEntityPathInFolder @$scope.rootFolder, entity
@@ -349,7 +349,7 @@ define [
}
deleteEntity: (entity, callback = (error) ->) ->
# We'll wait for the socket.io notification to
# We'll wait for the socket.io notification to
# delete from scope.
return @ide.queuedHttp {
method: "DELETE"
@@ -367,7 +367,7 @@ define [
folder_id: parent_folder.id
_csrf: window.csrfToken
}
_isChildFolder: (parent_folder, child_folder) ->
parent_path = @getEntityPath(parent_folder) or "" # null if root folder
child_path = @getEntityPath(child_folder) or "" # null if root folder
@@ -0,0 +1,17 @@
define [
"base"
], (App) ->
App.factory 'files', (ide) ->
Files =
getTeXFiles: () ->
texFiles = []
ide.fileTreeManager.forEachEntity (entity, folder, path) ->
if entity.type == 'doc' && entity?.name?.match?(/.*\.(tex|txt|md)/)
cloned = _.clone(entity)
cloned.path = path
texFiles.push cloned
return texFiles
return Files
@@ -1,5 +1,4 @@
define [
], () ->
define [], () ->
class LabelsManager
@@ -0,0 +1,13 @@
define [], () ->
class MetadataManager
constructor: (@ide, @$scope, @metadata) ->
@ide.socket.on 'broadcastDocMeta', (data) =>
@metadata.onBroadcastDocMeta data
@$scope.$on 'entity:deleted', @metadata.onEntityDeleted
@$scope.$on 'file:upload:complete', @metadata.fileUploadComplete
loadProjectMetaFromServer: () ->
@metadata.loadProjectMetaFromServer()
@@ -0,0 +1,49 @@
define [
"base"
], (App) ->
App.factory 'metadata', ($http, ide) ->
state = {documents: {}}
metadata = {state: state}
metadata.onBroadcastDocMeta = (data) ->
if data.docId? and data.meta?
state.documents[data.docId] = data.meta
metadata.onEntityDeleted = (e, entity) ->
if entity.type == 'doc'
delete state.documents[entity.id]
metadata.onFileUploadComplete = (e, upload) ->
if upload.entity_type == 'doc'
metadata.loadDocMetaFromServer upload.entity_id
metadata.getAllLabels = () ->
_.flatten(meta.labels for docId, meta of state.documents)
metadata.getAllPackages = () ->
packageCommandMapping = {}
for _docId, meta of state.documents
for packageName, commandSnippets of meta.packages
packageCommandMapping[packageName] = commandSnippets
return packageCommandMapping
metadata.loadProjectMetaFromServer = () ->
$http
.get("/project/#{window.project_id}/metadata")
.then (response) ->
{ data } = response
if data.projectMeta
for docId, docMeta of data.projectMeta
state.documents[docId] = docMeta
metadata.loadDocMetaFromServer = (docId) ->
$http
.post(
"/project/#{window.project_id}/doc/#{docId}/metadata",
{_csrf: window.csrfToken}
)
return metadata
-2
View File
@@ -1,12 +1,10 @@
define [
"moment"
"libs/angular-autocomplete/angular-autocomplete"
"libs/ui-bootstrap"
"libs/ng-context-menu-0.1.4"
"libs/underscore-1.3.3"
"libs/algolia-2.5.2"
"libs/jquery.storage"
"libs/fineuploader"
"libs/angular-sanitize-1.2.17"
"libs/angular-cookie"
"libs/passfield"
@@ -4,7 +4,7 @@ ace.define("ace/mode/latex_highlight_rules",["require","exports","module","ace/l
var oop = require("../lib/oop");
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var LatexHighlightRules = function() {
var LatexHighlightRules = function() {
this.$rules = {
"start" : [{
@@ -47,9 +47,9 @@ var LatexHighlightRules = function() {
token : "constant.character.escape",
regex : "\\\\(?:[^a-zA-Z]|[a-zA-Z]+)"
}, {
token : "error",
regex : "^\\s*$",
next : "start"
token : "error",
regex : "^\\s*$",
next : "start"
}, {
defaultToken : "string"
}]
@@ -76,16 +76,16 @@ oop.inherits(FoldMode, BaseFoldMode);
(function() {
this.foldingStartMarker = /^\s*\\(begin)|(section|subsection|paragraph)\b|{\s*$/;
this.foldingStartMarker = /^\s*\\(begin|section|subsection|subsubsection|paragraph|part|chapter)\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])
if (match[1] === "begin")
return this.latexBlock(session, row, match[0].length - 1);
if (match[2])
else if (match[1])
return this.latexSection(session, row, match[0].length - 1);
return this.openingBracketBlock(session, "{", row, match.index);
@@ -153,7 +153,7 @@ oop.inherits(FoldMode, BaseFoldMode);
};
this.latexSection = function(session, row, column) {
var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph"];
var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph", "\\part", "\\chapter"];
var stream = new TokenIterator(session, row, column);
var token = stream.getCurrentToken();
File diff suppressed because it is too large Load Diff
@@ -4,11 +4,11 @@
*
* Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
*/
.qq-uploader {
.qq-uploader-selector {
position: relative;
width: 100%;
}
.qq-uploader {
.qq-uploader-selector {
text-align: center;
.drag-here {
border: 1px dashed #666;
@@ -18,7 +18,7 @@
margin-top: 6px;
}
}
/*.qq-upload-button {
/*.qq-upload-button-selector {
display: block;
width: 105px;
padding: 7px 0;
@@ -27,13 +27,13 @@
border-bottom: 1px solid #DDD;
color: #FFF;
}
.qq-upload-button-hover {
.qq-upload-button-hover-selector {
background: #CC0000;
}
.qq-upload-button-focus {
.qq-upload-button-focus-selector {
outline: 1px dotted #000000;
}*/
.qq-upload-drop-area, .qq-upload-extra-drop-area {
.qq-upload-drop-area-selector, .qq-upload-extra-drop-area-selector {
position: absolute;
top: 0;
left: 0;
@@ -44,7 +44,7 @@
background: @orange;
text-align: center;
}
.qq-upload-drop-area span {
.qq-upload-drop-area-selector span {
display: block;
position: absolute;
top: 50%;
@@ -53,7 +53,7 @@
font-size: 16px;
color: white;
}
.qq-upload-extra-drop-area {
.qq-upload-extra-drop-area-selector {
position: relative;
margin-top: 50px;
font-size: 16px;
@@ -61,15 +61,15 @@
height: 20px;
min-height: 40px;
}
.qq-upload-drop-area-active {
.qq-upload-drop-area-active-selector {
background: darken(@orange, 15%);
}
.qq-upload-list {
.qq-upload-list-selector {
margin: 0;
padding: 0;
list-style: none;
}
.qq-upload-list li {
.qq-upload-list-selector li {
margin: 0;
margin-top: 10px;
padding: 9px;
@@ -77,74 +77,74 @@
font-size: 16px;
background-color: @gray-lightest;
}
.qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-failed-text, .qq-upload-finished, .qq-upload-delete {
.qq-upload-file-selector, .qq-upload-spinner-selector, .qq-upload-size-selector, .qq-upload-cancel-selector, .qq-upload-retry-selector, .qq-upload-failed-text-selector, .qq-upload-finished-selector, .qq-upload-delete-selector {
margin-right: 12px;
}
.qq-upload-file {
.qq-upload-file-selector {
}
.qq-upload-spinner {
.qq-upload-spinner-selector {
display: inline-block;
width: 15px;
height: 15px;
vertical-align: text-bottom;
}
.qq-drop-processing {
.qq-drop-processing-selector {
display: none;
}
.qq-drop-processing-spinner {
.qq-drop-processing-spinner-selector {
display: inline-block;
background: url("processing.gif");
width: 24px;
height: 24px;
vertical-align: text-bottom;
}
.qq-upload-finished {
.qq-upload-finished-selector {
display:none;
width:15px;
height:15px;
vertical-align:text-bottom;
}
.qq-upload-retry, .qq-upload-delete {
.qq-upload-retry-selector, .qq-upload-delete-selector {
display: none;
// color: #000000;
}
.qq-upload-cancel, .qq-upload-delete {
.qq-upload-cancel-selector, .qq-upload-delete-selector {
// color: #000000;
}
.qq-upload-retryable .qq-upload-retry {
.qq-upload-retryable-selector .qq-upload-retry-selector {
display: inline;
}
.qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-delete {
.qq-upload-size-selector, .qq-upload-cancel-selector, .qq-upload-retry-selector, .qq-upload-delete-selector {
font-size: 12px;
font-weight: normal;
}
.qq-upload-failed-text {
.qq-upload-failed-text-selector {
display: none;
font-style: italic;
font-weight: bold;
}
.qq-upload-failed-icon {
.qq-upload-failed-icon-selector {
display:none;
width:15px;
height:15px;
vertical-align:text-bottom;
}
.qq-upload-fail .qq-upload-failed-text {
.qq-upload-fail-selector .qq-upload-failed-text-selector {
display: inline;
}
.qq-upload-retrying .qq-upload-failed-text {
.qq-upload-retrying-selector .qq-upload-failed-text-selector {
display: inline;
color: #D60000;
}
.qq-upload-list li.qq-upload-success {
.qq-upload-list-selector li.qq-upload-success-selector {
background-color: @green;
color: #FFFFFF;
}
.qq-upload-list li.qq-upload-fail {
.qq-upload-list-selector li.qq-upload-fail-selector {
background-color: @red;
color: #FFFFFF;
}
.qq-progress-bar {
.qq-progress-bar-selector {
width: 0%;
height: @line-height-computed;
margin-bottom: @line-height-computed / 2;
@@ -156,5 +156,9 @@
.box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
.transition(width .6s ease);
border-radius: @border-radius-base 0 0 @border-radius-base;
display: none;
}
a.qq-btn {
&:hover {
cursor: pointer !important;
}
}
@@ -1,5 +1,4 @@
Settings = require('settings-sharelatex')
redis = require('redis-sharelatex')
logger = require("logger-sharelatex")
Async = require('async')
@@ -1,4 +1,4 @@
BASE_URL = "http://localhost:3000"
BASE_URL = "http://#{process.env["HTTP_TEST_HOST"] or "localhost"}:3000"
module.exports = require("request").defaults({
baseUrl: BASE_URL,
followRedirect: false
@@ -1,119 +0,0 @@
chai = require('chai')
chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Labels/LabelsController"
SandboxedModule = require('sandboxed-module')
describe 'LabelsController', ->
beforeEach ->
@projectId = 'somekindofid'
@EditorRealTimeController = {
emitToRoom: sinon.stub()
}
@LabelsHandler = {
getAllLabelsForProject: sinon.stub()
getLabelsForDoc: sinon.stub()
}
@LabelsController = SandboxedModule.require modulePath, requires:
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
'../Editor/EditorRealTimeController': @EditorRealTimeController
'./LabelsHandler': @LabelsHandler
describe 'getAllLabels', ->
beforeEach ->
@fakeLabels = {'somedoc': ['a_label']}
@LabelsHandler.getAllLabelsForProject = sinon.stub().callsArgWith(1, null, @fakeLabels)
@req = {params: {project_id: @projectId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getAllLabelsForProject', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@LabelsHandler.getAllLabelsForProject.callCount.should.equal 1
@LabelsHandler.getAllLabelsForProject.calledWith(@projectId).should.equal true
it 'should call not call next with an error', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@next.callCount.should.equal 0
it 'should send a json response', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@res.json.callCount.should.equal 1
expect(@res.json.lastCall.args[0]).to.have.all.keys ['projectId', 'projectLabels']
describe 'when LabelsHandler.getAllLabelsForProject produces an error', ->
beforeEach ->
@LabelsHandler.getAllLabelsForProject = sinon.stub().callsArgWith(1, new Error('woops'))
@req = {params: {project_id: @projectId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getAllLabelsForProject', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@LabelsHandler.getAllLabelsForProject.callCount.should.equal 1
@LabelsHandler.getAllLabelsForProject.calledWith(@projectId).should.equal true
it 'should call next with an error', ->
@LabelsController.getAllLabels(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not send a json response', ->
@LabelsController.getAllLabels(@req, @res, @next)
@res.json.callCount.should.equal 0
describe 'broadcastLabelsForDoc', ->
beforeEach ->
@LabelsHandler.getLabelsForDoc = sinon.stub().callsArgWith(2, null, @fakeLabels)
@EditorRealTimeController.emitToRoom = sinon.stub()
@docId = 'somedoc'
@req = {params: {project_id: @projectId, doc_id: @docId}}
@res = {sendStatus: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getLabelsForDoc', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@LabelsHandler.getLabelsForDoc.callCount.should.equal 1
@LabelsHandler.getLabelsForDoc.calledWith(@projectId).should.equal true
it 'should call not call next with an error', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@next.callCount.should.equal 0
it 'should send a success response', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@res.sendStatus.callCount.should.equal 1
@res.sendStatus.calledWith(200).should.equal true
it 'should emit a message to room', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
lastCall = @EditorRealTimeController.emitToRoom.lastCall
expect(lastCall.args[0]).to.equal @projectId
expect(lastCall.args[1]).to.equal 'broadcastDocLabels'
expect(lastCall.args[2]).to.have.all.keys ['docId', 'labels']
describe 'when LabelsHandler.getLabelsForDoc produces an error', ->
beforeEach ->
@LabelsHandler.getLabelsForDoc = sinon.stub().callsArgWith(2, new Error('woops'))
@EditorRealTimeController.emitToRoom = sinon.stub()
@docId = 'somedoc'
@req = {params: {project_id: @projectId, doc_id: @docId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getLabelsForDoc', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@LabelsHandler.getLabelsForDoc.callCount.should.equal 1
@LabelsHandler.getLabelsForDoc.calledWith(@projectId).should.equal true
it 'should call next with an error', ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not send a json response', ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@res.json.callCount.should.equal 0
@@ -1,134 +0,0 @@
chai = require('chai')
chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Labels/LabelsHandler"
SandboxedModule = require('sandboxed-module')
describe 'LabelsHandler', ->
beforeEach ->
@projectId = 'someprojectid'
@docId = 'somedocid'
@ProjectEntityHandler = {
getAllDocs: sinon.stub()
getDoc: sinon.stub()
}
@DocumentUpdaterHandler = {
flushDocToMongo: sinon.stub()
}
@LabelsHandler = SandboxedModule.require modulePath, requires:
'../Project/ProjectEntityHandler': @ProjectEntityHandler
'../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler
describe 'extractLabelsFromDoc', ->
beforeEach ->
@lines = [
'one',
'two',
'three \\label{aaa}',
'four five',
'\\label{bbb}',
'six seven'
]
it 'should extract all the labels', ->
docLabels = @LabelsHandler.extractLabelsFromDoc @lines
expect(docLabels).to.deep.equal ['aaa', 'bbb']
describe 'extractLabelsFromProjectDocs', ->
beforeEach ->
@docs = {
'doc_one': {
_id: 'id_one',
lines: ['one', '\\label{aaa} two', 'three']
},
'doc_two': {
_id: 'id_two',
lines: ['four']
},
'doc_three': {
_id: 'id_three',
lines: ['\\label{bbb}', 'five six', 'seven eight \\label{ccc} nine']
}
}
it 'should extract all the labels', ->
projectLabels = @LabelsHandler.extractLabelsFromProjectDocs @docs
expect(projectLabels).to.deep.equal {
'id_one': ['aaa'],
'id_two': [],
'id_three': ['bbb', 'ccc']
}
describe 'getLabelsForDoc', ->
beforeEach ->
@fakeLines = ['one', '\\label{aaa}', 'two']
@fakeLabels = ['aaa']
@DocumentUpdaterHandler.flushDocToMongo = sinon.stub().callsArgWith(2, null)
@ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @fakeLines)
@LabelsHandler.extractLabelsFromDoc = sinon.stub().returns(@fakeLabels)
@call = (callback) =>
@LabelsHandler.getLabelsForDoc @projectId, @docId, callback
it 'should not produce an error', (done) ->
@call (err, docLabels) =>
expect(err).to.equal null
done()
it 'should produce docLabels', (done) ->
@call (err, docLabels) =>
expect(docLabels).to.equal @fakeLabels
done()
it 'should call flushDocToMongo', (done) ->
@call (err, docLabels) =>
@DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 1
@DocumentUpdaterHandler.flushDocToMongo.calledWith(@projectId, @docId).should.equal true
done()
it 'should call getDoc', (done) ->
@call (err, docLabels) =>
@ProjectEntityHandler.getDoc.callCount.should.equal 1
@ProjectEntityHandler.getDoc.calledWith(@projectId, @docId).should.equal true
done()
it 'should call extractLabelsFromDoc', (done) ->
@call (err, docLabels) =>
@LabelsHandler.extractLabelsFromDoc.callCount.should.equal 1
@LabelsHandler.extractLabelsFromDoc.calledWith(@fakeLines).should.equal true
done()
describe 'getAllLabelsForProject', ->
beforeEach ->
@fakeDocs = {
'doc_one': {lines: ['\\label{aaa}']}
}
@fakeLabels = ['aaa']
@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @fakeDocs)
@LabelsHandler.extractLabelsFromProjectDocs = sinon.stub().returns(@fakeLabels)
@call = (callback) =>
@LabelsHandler.getAllLabelsForProject @projectId, callback
it 'should not produce an error', (done) ->
@call (err, projectLabels) =>
expect(err).to.equal null
done()
it 'should produce projectLabels', (done) ->
@call (err, projectLabels) =>
expect(projectLabels).to.equal @fakeLabels
done()
it 'should call getAllDocs', (done) ->
@call (err, projectLabels) =>
@ProjectEntityHandler.getAllDocs.callCount.should.equal 1
@ProjectEntityHandler.getAllDocs.calledWith(@projectId).should.equal true
done()
it 'should call extractLabelsFromDoc', (done) ->
@call (err, docLabels) =>
@LabelsHandler.extractLabelsFromProjectDocs.callCount.should.equal 1
@LabelsHandler.extractLabelsFromProjectDocs.calledWith(@fakeDocs).should.equal true
done()
@@ -0,0 +1,119 @@
chai = require('chai')
chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Metadata/MetaController"
SandboxedModule = require('sandboxed-module')
describe 'MetaController', ->
beforeEach ->
@projectId = 'somekindofid'
@EditorRealTimeController = {
emitToRoom: sinon.stub()
}
@MetaHandler = {
getAllMetaForProject: sinon.stub()
getMetaForDoc: sinon.stub()
}
@MetadataController = SandboxedModule.require modulePath, requires:
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
'../Editor/EditorRealTimeController': @EditorRealTimeController
'./MetaHandler': @MetaHandler
describe 'getMetadata', ->
beforeEach ->
@fakeLabels = {'somedoc': ['a_label']}
@MetaHandler.getAllMetaForProject = sinon.stub().callsArgWith(1, null, @fakeLabels)
@req = {params: {project_id: @projectId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call MetaHandler.getAllMetaForProject', () ->
@MetadataController.getMetadata(@req, @res, @next)
@MetaHandler.getAllMetaForProject.callCount.should.equal 1
@MetaHandler.getAllMetaForProject.calledWith(@projectId).should.equal true
it 'should call not call next with an error', () ->
@MetadataController.getMetadata(@req, @res, @next)
@next.callCount.should.equal 0
it 'should send a json response', () ->
@MetadataController.getMetadata(@req, @res, @next)
@res.json.callCount.should.equal 1
expect(@res.json.lastCall.args[0]).to.have.all.keys ['projectId', 'projectMeta']
describe 'when MetaHandler.getAllMetaForProject produces an error', ->
beforeEach ->
@MetaHandler.getAllMetaForProject = sinon.stub().callsArgWith(1, new Error('woops'))
@req = {params: {project_id: @projectId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call MetaHandler.getAllMetaForProject', () ->
@MetadataController.getMetadata(@req, @res, @next)
@MetaHandler.getAllMetaForProject.callCount.should.equal 1
@MetaHandler.getAllMetaForProject.calledWith(@projectId).should.equal true
it 'should call next with an error', ->
@MetadataController.getMetadata(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not send a json response', ->
@MetadataController.getMetadata(@req, @res, @next)
@res.json.callCount.should.equal 0
describe 'broadcastMetadataForDoc', ->
beforeEach ->
@MetaHandler.getMetaForDoc = sinon.stub().callsArgWith(2, null, @fakeLabels)
@EditorRealTimeController.emitToRoom = sinon.stub()
@docId = 'somedoc'
@req = {params: {project_id: @projectId, doc_id: @docId}}
@res = {sendStatus: sinon.stub()}
@next = sinon.stub()
it 'should call MetaHandler.getMetaForDoc', () ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@MetaHandler.getMetaForDoc.callCount.should.equal 1
@MetaHandler.getMetaForDoc.calledWith(@projectId).should.equal true
it 'should call not call next with an error', () ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@next.callCount.should.equal 0
it 'should send a success response', () ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@res.sendStatus.callCount.should.equal 1
@res.sendStatus.calledWith(200).should.equal true
it 'should emit a message to room', () ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
lastCall = @EditorRealTimeController.emitToRoom.lastCall
expect(lastCall.args[0]).to.equal @projectId
expect(lastCall.args[1]).to.equal 'broadcastDocMeta'
expect(lastCall.args[2]).to.have.all.keys ['docId', 'meta']
describe 'when MetaHandler.getMetaForDoc produces an error', ->
beforeEach ->
@MetaHandler.getMetaForDoc = sinon.stub().callsArgWith(2, new Error('woops'))
@EditorRealTimeController.emitToRoom = sinon.stub()
@docId = 'somedoc'
@req = {params: {project_id: @projectId, doc_id: @docId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call MetaHandler.getMetaForDoc', () ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@MetaHandler.getMetaForDoc.callCount.should.equal 1
@MetaHandler.getMetaForDoc.calledWith(@projectId).should.equal true
it 'should call next with an error', ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not send a json response', ->
@MetadataController.broadcastMetadataForDoc(@req, @res, @next)
@res.json.callCount.should.equal 0
@@ -0,0 +1,243 @@
chai = require('chai')
chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Metadata/MetaHandler"
SandboxedModule = require('sandboxed-module')
describe 'MetaHandler', ->
beforeEach ->
@projectId = 'someprojectid'
@docId = 'somedocid'
@ProjectEntityHandler = {
getAllDocs: sinon.stub()
getDoc: sinon.stub()
}
@DocumentUpdaterHandler = {
flushDocToMongo: sinon.stub()
}
@packageMapping =
foo: [
{
caption: '\\bar'
snippet: '\\bar'
meta: 'foo-cmd'
score: 12
}, {
caption: '\\bat[]{}'
snippet: '\\bar[$1]{$2}'
meta: 'foo-cmd'
score: 10
}
],
baz: [
{
caption: '\\longercommandtest{}'
snippet: '\\longercommandtest{$1}'
meta: 'baz-cmd'
score: 50
}
]
@MetaHandler = SandboxedModule.require modulePath, requires:
'../Project/ProjectEntityHandler': @ProjectEntityHandler
'../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler
'./packageMapping': @packageMapping
describe 'extractMetaFromDoc', ->
beforeEach ->
@lines = [
'\\usepackage{foo}'
'\\usepackage{amsmath, booktabs}'
'one'
'two'
'three \\label{aaa}'
'four five'
'\\label{bbb}'
'six seven'
]
it 'should extract all the labels and packages', ->
docMeta = @MetaHandler.extractMetaFromDoc @lines
expect(docMeta).to.deep.equal {
labels: ['aaa', 'bbb']
packages:
foo: [
{
caption: '\\bar'
snippet: '\\bar'
meta: 'foo-cmd'
score: 12
}, {
caption: '\\bat[]{}'
snippet: '\\bar[$1]{$2}'
meta: 'foo-cmd'
score: 10
}
]
}
describe 'extractMetaFromProjectDocs', ->
beforeEach ->
@docs =
'doc_one':
_id: 'id_one'
lines: ['one', '\\label{aaa} two', 'three']
'doc_two':
_id: 'id_two'
lines: ['four']
'doc_three':
_id: 'id_three'
lines: [
'\\label{bbb}'
'five six'
'seven eight \\label{ccc} nine'
]
'doc_four':
_id: 'id_four'
lines: [
'\\usepackage[width=\\textwidth]{baz}'
'\\usepackage{amsmath}'
]
'doc_five':
_id: 'id_five'
lines: [
'\\usepackage{foo,baz}'
'\\usepackage[options=foo]{hello}'
'some text'
'\\section{this}\\label{sec:intro}'
'In Section \\ref{sec:intro} we saw'
'nothing'
]
it 'should extract all metadata', ->
projectMeta = @MetaHandler.extractMetaFromProjectDocs @docs
expect(projectMeta).to.deep.equal {
'id_one': {labels: ['aaa'], packages: {}}
'id_two': {labels: [], packages: {}}
'id_three': {labels: ['bbb', 'ccc'], packages: {}}
'id_four':
labels: []
packages:
baz: [{
caption: '\\longercommandtest{}'
snippet: '\\longercommandtest{$1}'
meta: 'baz-cmd'
score: 50}]
'id_five':
labels: ['sec:intro']
packages:
foo: [
{
caption: '\\bar'
snippet: '\\bar'
meta: 'foo-cmd'
score: 12
}, {
caption: '\\bat[]{}'
snippet: '\\bar[$1]{$2}'
meta: 'foo-cmd'
score: 10
}
]
baz: [
{
caption: '\\longercommandtest{}'
snippet: '\\longercommandtest{$1}'
meta: 'baz-cmd'
score: 50
}
]
}
describe 'getMetaForDoc', ->
beforeEach ->
@fakeLines = ['\\usepackage{abc}', 'one', '\\label{aaa}', 'two']
@fakeMeta = {labels: ['aaa'], packages: ['abc']}
@DocumentUpdaterHandler.flushDocToMongo = sinon.stub().callsArgWith 2, null
@ProjectEntityHandler.getDoc = sinon.stub().callsArgWith 2, null, @fakeLines
@MetaHandler.extractMetaFromDoc = sinon.stub().returns @fakeMeta
@call = (callback) =>
@MetaHandler.getMetaForDoc @projectId, @docId, callback
it 'should not produce an error', (done) ->
@call (err, docMeta) =>
expect(err).to.equal null
done()
it 'should produce docMeta', (done) ->
@call (err, docMeta) =>
expect(docMeta).to.equal @fakeMeta
done()
it 'should call flushDocToMongo', (done) ->
@call (err, docMeta) =>
@DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 1
@DocumentUpdaterHandler.flushDocToMongo.calledWith(@projectId, @docId).should.equal true
done()
it 'should call getDoc', (done) ->
@call (err, docMeta) =>
@ProjectEntityHandler.getDoc.callCount.should.equal 1
@ProjectEntityHandler.getDoc.calledWith(@projectId, @docId).should.equal true
done()
it 'should call extractMetaFromDoc', (done) ->
@call (err, docMeta) =>
@MetaHandler.extractMetaFromDoc.callCount.should.equal 1
@MetaHandler.extractMetaFromDoc.calledWith(@fakeLines).should.equal true
done()
describe 'getAllMetaForProject', ->
beforeEach ->
@fakeDocs =
'doc_one':
lines: [
'\\usepackage[some-options,more=foo]{foo}'
'\\label{aaa}'
]
@fakeMeta =
labels: ['aaa']
packages:
foo: [
{
caption: '\\bar'
snippet: '\\bar'
meta: 'foo-cmd'
score: 12
}, {
caption: '\\bat[]{}'
snippet: '\\bar[$1]{$2}'
meta: 'foo-cmd'
score: 10
}
]
@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith 1, null
@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith 1, null, @fakeDocs
@MetaHandler.extractMetaFromProjectDocs = sinon.stub().returns @fakeMeta
@call = (callback) =>
@MetaHandler.getAllMetaForProject @projectId, callback
it 'should not produce an error', (done) ->
@call (err, projectMeta) =>
expect(err).to.equal null
done()
it 'should produce projectMeta', (done) ->
@call (err, projectMeta) =>
expect(projectMeta).to.equal @fakeMeta
done()
it 'should call getAllDocs', (done) ->
@call (err, projectMeta) =>
@ProjectEntityHandler.getAllDocs.callCount.should.equal 1
@ProjectEntityHandler.getAllDocs.calledWith(@projectId).should.equal true
done()
it 'should call extractMetaFromDoc', (done) ->
@call (err, docMeta) =>
@MetaHandler.extractMetaFromProjectDocs.callCount.should.equal 1
@MetaHandler.extractMetaFromProjectDocs.calledWith(@fakeDocs).should.equal true
done()
@@ -44,6 +44,7 @@ describe "SubscriptionController", ->
@RecurlyWrapper =
sign: sinon.stub().callsArgWith(1, null, "somthing")
getSubscription: sinon.stub().callsArgWith(2, null, {})
@SubscriptionViewModelBuilder =
buildUsersSubscriptionViewModel:sinon.stub().callsArgWith(1, null, @activeRecurlySubscription)
@@ -55,7 +56,7 @@ describe "SubscriptionController", ->
collaborator:"COLLABORATORCODEHERE"
apis:
recurly:
subdomain:"sl.recurly.com"
subdomain:"sl"
siteUrl: "http://de.sharelatex.dev:3000"
gaExperiments:{}
@GeoIpLookup =
@@ -118,34 +119,6 @@ describe "SubscriptionController", ->
@UserGetter.getUser.callCount.should.equal 0
done()
describe "editBillingDetailsPage", ->
describe "with a user with a subscription", ->
beforeEach (done) ->
@LimitationsManager.userHasSubscription.callsArgWith(1, null, true)
@user._id = @activeRecurlySubscription.account.account_code
@res.callback = done
@SubscriptionController.editBillingDetailsPage(@req, @res)
it "should render the edit billing details page", ->
@res.rendered.should.equal true
@res.renderedTemplate.should.equal "subscriptions/edit-billing-details"
it "should set the correct variables for the template", ->
should.exist @res.renderedVariables.signature
@res.renderedVariables.successURL.should.equal "#{@settings.siteUrl}/user/subscription/billing-details/update"
@res.renderedVariables.user.id.should.equal @user._id
describe "with a user without subscription", ->
beforeEach (done) ->
@res.callback = done
@LimitationsManager.userHasSubscription.callsArgWith(1, null, false)
@SubscriptionController.reactivateSubscription @req, @res
it "should redirect to the subscription dashboard", ->
@res.redirected.should.equal true
@res.redirectedTo.should.equal "/user/subscription"
describe "paymentPage", ->
beforeEach ->
@req.headers = {}
@@ -255,7 +228,12 @@ describe "SubscriptionController", ->
describe "with an existing subscription", ->
beforeEach (done)->
@res.callback = done
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true)
@settings.apis.recurly.subdomain = 'test'
@userSub = {account: {hosted_login_token: 'abcd'}}
@RecurlyWrapper.getSubscription = sinon.stub()
.callsArgWith(2, null, @userSub)
@LimitationsManager.userHasSubscriptionOrIsGroupMember
.callsArgWith(1, null, true, {})
@SubscriptionController.userSubscriptionPage @req, @res
@@ -266,7 +244,7 @@ describe "SubscriptionController", ->
beforeEach (done) ->
@res.callback = done
@SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, @activeRecurlySubscription)
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true)
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true, {})
@SubscriptionController.userSubscriptionPage @req, @res
it "should render the dashboard", (done)->
@@ -281,7 +259,7 @@ describe "SubscriptionController", ->
beforeEach (done) ->
@res.callback = done
@SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, @activeRecurlySubscription)
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true)
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true, {})
@SubscriptionController.userSubscriptionPage @req, @res
it "should render the dashboard", ->